Shops near you – geographic features of GeoDjango

Check out the new site at https://rkblog.dev.

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_NAME
    And execute:
    \password postgres
    Set the new password and you are done.
The database is ready to use.

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',
    }
}
Also you have to add 'django.contrib.gis', into INSTALLED_APPS. After that you can run syncdb or any other planned setup operation.

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
But I've mentioned coordinates, so our model looks like so:
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
The new thing is PointField under location. It's capable of storing coordinates. Also I've defined an additional query manager - "gis". GeoManager allows executing GIS/GEOS queries. Default manager can't do that.
Shop.gis.filter() # with GIS queries
Shop.objects.filter() # only standard queries
If you plan to use GIS queries a lot you can set the GeoManager under "objects".

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:
Adding test shops to database
Adding test shops to database

"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:
POINT (21.0122287000000014 52.2296756000000002)
Now we have a complete set of data for our application.

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 %}
And a form class:
from django import forms


class AddressForm(forms.Form):
    address = forms.CharField()
And now if I enter an address close to some existing shops I'll get them on the list:
GeoDjango in action

GeoDjango in action

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>
It's a very very "quick" use of Google Maps API. I've created a map, centered it on queried address coordinates and added markers for given address and all returned shops. In production code this would be in a JS file, and the data with shops would be passed in for example JSON format via AJAX request (or JS function arguments).
Shops and given address marked on a Google Map

Shops and given address marked on a Google Map

Google Maps API is described on developers.google.com/maps/.
RkBlog

Django web framework tutorials, 3 August 2012


Check out the new site at https://rkblog.dev.
Comment article
Comment article RkBlog main page Search RSS Contact