Shops near you – geographic features of GeoDjango
Boom on mobile devices and widespread Internet access created a demand for applications aware of user geographic location. Nowadays using modern web frameworks with magical powers you can make such geo-enabled web applications easily. Django offers a special sub-framework called geodjango. There you will find an enormous amount of features of "geographic" nature.
In this article I'll present a simple application that will use a part of GeoDjango to search and display shops closest to given address.
Database
GeoDjango starts in the database (for this application). To keep geographic coordinates in a database it needs a special engine (GEOS) to do it. Django documentation describes cases for various databases, but I'm going to use PostgreSQL, which seems to be very good at it.
We need to have a postgresql server installed. We also need postgis extension. For Debian/Ubuntu and alike systems it will be postgresql-*-postgis, where * is the current PostgreSQL version (for Ubuntu 12.04 postgresql-9.1-postgis).
When you install that extension you will have to configure postgres and create a postgis-enabled database. The longer version is in the documentation. I'll show you a quick path for a Debian/Ubuntu system.
- Open a terminal and switch to postgres user:
sudo -i su postgres cd
- Download (wget) configuration script - create_template_postgis-debian.sh from Django documentation.
- Launch it:
chmod 755 ./create_template_postgis-debian.sh ./create_template_postgis-debian.sh
- Create postgis database:
createdb -T template_postgis DATABASE_NAME
- Set a password for postgres user (if you already didn't do that):
psql DATABASE_NAMEAnd execute:\password postgresSet the new password and you are done.
Django configuration
Configuration is quite simple. The new part is the database engine - django.contrib.gis.db.backends.postgis. In my case for the demo app it looked like so:DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'postgis_test',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
}
}
GIS-enabled application
We have a special database "type", we have a special Django configuration - and now we will use to. I'll show you a simple Django application - a list of shops. Each shop will have a name and an address (street, city). In the model we will also store geographic coordinates - used for maps and for "geographic" queries like filtering/limiting by distance.
Model
Typical model would look like so:from django.db import models
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
def __unicode__(self):
return self.name
from django.contrib.gis.db import models as gis_models
from django.contrib.gis import geos
from django.db import models
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
location = gis_models.PointField(u"longitude/latitude",
geography=True, blank=True, null=True)
gis = gis_models.GeoManager()
objects = models.Manager()
def __unicode__(self):
return self.name
Shop.gis.filter() # with GIS queries
Shop.objects.filter() # only standard queries
Now we can launch syncdb and create a table for the model.
Creating data
Using the Django admin panel I've created few test shop entries:"location" field is empty. Most of map services like Google Maps offer geocoding services - they turn address into coordinates. I've used geopy to geocode the address with Google Maps geocoder. And after getting those coordinates I can save them in the database using the "location" field.
To add "automatic" geocoding I defined a save method for my model (you can also use a signal):from urllib2 import URLError
from django.contrib.gis.db import models as gis_models
from django.contrib.gis import geos
from django.db import models
from geopy.geocoders.google import Google
from geopy.geocoders.google import GQueryError
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
location = gis_models.PointField(u"longitude/latitude",
geography=True, blank=True, null=True)
gis = gis_models.GeoManager()
objects = models.Manager()
def __unicode__(self):
return self.name
def save(self, **kwargs):
if not self.location:
address = u'%s %s' % (self.city, self.address)
address = address.encode('utf-8')
geocoder = Google()
try:
_, latlon = geocoder.geocode(address)
except (URLError, GQueryError, ValueError):
pass
else:
point = "POINT(%s %s)" % (latlon[1], latlon[0])
self.location = geos.fromstr(point)
super(Shop, self).save()
In the save method I use geopy to geocode the address and if successful I'm assigning a "POINT" object to a PointField :) Just don't switch longitude with latitude.
Now if address is correct geocoded points should show up for shops, like for example:From Python level those points are available as location.x as location.y.
GEO-Queries
Now we will use the magic of the PointField to make a query that will return shops closest to given address (coordinates). If you would store coordinates in the "old" way as two text fields it wouldn't be so easy to do it...
In our example I'll create a view with a form for address user will search form. The backend code will geocode the address and query for closest shops.
The view looks like so:from urllib2 import URLError
from django.contrib.gis import geos
from django.contrib.gis import measure
from django.shortcuts import render_to_response
from django.template import RequestContext
from geopy.geocoders.google import Google
from geopy.geocoders.google import GQueryError
from shops import forms
from shops import models
def geocode_address(address):
address = address.encode('utf-8')
geocoder = Google()
try:
_, latlon = geocoder.geocode(address)
except (URLError, GQueryError, ValueError):
return None
else:
return latlon
def get_shops(longitude, latitude):
current_point = geos.fromstr("POINT(%s %s)" % (longitude, latitude))
distance_from_point = {'km': 10}
shops = models.Shop.gis.filter(location__distance_lte=(current_point, measure.D(**distance_from_point)))
shops = shops.distance(current_point).order_by('distance')
return shops.distance(current_point)
def home(request):
form = forms.AddressForm()
shops = []
if request.POST:
form = forms.AddressForm(request.POST)
if form.is_valid():
address = form.cleaned_data['address']
location = geocode_address(address)
if location:
latitude, longitude = location
shops = get_shops(longitude, latitude)
return render_to_response(
'home.html',
{'form': form, 'shops': shops},
context_instance=RequestContext(request))
We have a "home" view mapped under the main / url. It's a typical view with form support. The "new" code is located in get_shops function. In that function I query for shops, but using the GIS manager - models.Shop.gis. Using the "location" field I filter the query to return shops not further away than 10 km, and filtered by distance to given address (geocoded to coordinates). Used methods are just a few from many available in GeoDjango. More detailed presentation is available on chicagodjango.com.
And there is also a template:<h1>Shop GEO-Test</h1>
<form method="post" action="./">
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit" value="Search" />
</form>
{% if shops %}
<h2>Shops near you</h2>
<ul>
{% for shop in shops %}
<li><b>{{ shop.name }}</b>: distance: {{ shop.distance }}</li>
{% endfor %}
</ul>
{% endif %}
from django import forms
class AddressForm(forms.Form):
address = forms.CharField()
It was a bit long way to get such a nice feature, but with the help of Django it wasn't messy or hard.
Google Maps
A simple list of shops may not be a cool presentation of results. As we have coordinates we can use a map system like Google Maps or OpenStreetMap to present shops and queried address on a map.
I've added longitude/latitude of queried address to the template context. After that I used such code to display a Google Map with all shops marked on the map:
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
$(document).ready(function() {
var latlng = new google.maps.LatLng("{{ latitude }}", "{{ longitude }}");
var mapOptions = {
zoom: 15,
center: latlng,
mapTypeControl: false,
navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL},
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map($('.map')[0], mapOptions);
var marker = new google.maps.Marker({
position: latlng,
map: map,
title:"Jesteś tutaj"
});
{% for shop in shops %}
latlng = new google.maps.LatLng("{{ shop.location.y }}", "{{ shop.location.x }}");
new google.maps.Marker({
position: latlng,
map: map,
title:"{{ shop.name }}"
});
{% endfor %}
});
</script>
<div class="map" style="width: 400px; height: 400px;"></div>
Comment article