Adding GeoDjango to Existing Project

November 18, 2011

I have a need to add GeoDjango functionality to an existing Django project. This runs on a Debian Lenny server, and uses a Postgres 8.4 database

Install packages

I needed to install the following packages:

# apt-get install libgeos-3.2.0 proj postgis gdal-bin postgresql-8.4-postgis

You can test this is all installed correctly using a Django python shell:

>>> from django.contrib.gis.gdal import HAS_GDAL
>>> print HAS_GDAL
True

If you're working on a development machine with the database on a different box, you'll need to do this on the database box only. It's not needed where the web server runs.

Add GIS functionality to database

Next, we need to change the existing database (bbr) so that it has GIS functionality:

# su postgres
$ createlang -d bbr plpgsql
$ psql -d bbr -f /usr/share/postgresql/8.4/contrib/postgis-1.5/postgis.sql
$ psql -d bbr -f /usr/share/postgresql/8.4/contrib/postgis-1.5/spatial_ref_sys.sql
$ psql
postgres=# \c bbr
bbr=# GRANT ALL ON geometry_columns TO PUBLIC;
bbr=# GRANT ALL ON geography_columns TO PUBLIC;
bbr=# GRANT ALL ON spatial_ref_sys TO PUBLIC;

Django Project Changes

You will need to change the database driver in your settings.py. Here I'm using postgres so I need to configure the connection to use postgis.

DATABASES = {
    'default': {
         'ENGINE': 'django.contrib.gis.db.backends.postgis',
         'NAME': 'database_name',
         'USER': 'database_user',
         'PASSWORD' : 'password',
         'HOST' : 'localhost',
         'PORT' : 5432,
     }
}

You will also need to add the following to your list of INSTALLED_APPS:

'django.contrib.gis',

Django Model Changes

I already have text fields that hold latitude and longitude. I need to convert these to the appropriate GIS data types in the database. South works well for database migration, even for GIS types.

As I'm still messing with GeoDjango, I've added two new data types. One is a standard geometry type, the second will use the geography type. This uses more complex maths and has some query limitations, but is more accurate.

from django.contrib.gis.db import models as geomodels
from django.contrib.gis.geos import *

latitude = models.CharField(max_length=15, blank=True, null=True)
longitude = models.CharField(max_length=15, blank=True, null=True) 
map_location = geomodels.PointField(dim=3, blank=True, null=True)
point = geomodels.PointField(dim=3, geography=True, blank=True, null=True)

You will also need to override the objects attribute so that is is GIS aware.

objects = geomodels.GeoManager()

In the save() method, I'm automatically populating the two new data types from the strings I had before.

def save(self):
    if self.latitude != None and len(self.latitude) > 0:
        lString = 'POINT(%s %s)' % (self.longitude.strip(), self.latitude.strip())
        self.map_location = fromstr(lString)
        self.point = fromstr(lString)
    self.last_modified = datetime.now()
    super(Band, self).save()

Testing

In order to use the postgis features with the Django auto test system, you'll need a template_postgis database. To set this up, do the following:

# su postgres
$ createlang -d template_postgis plpgsql
$ psql -d template_postgis -f /usr/share/postgresql/8.4/contrib/postgis-1.5/postgis.sql
$ psql -d template_postgis -f /usr/share/postgresql/8.4/contrib/postgis-1.5/spatial_ref_sys.sql
$ psql
postgres=# update pg_database set datistemplate=true where datname='template_postgis';
postgres=# \c template_postgis
template_postgis=# GRANT ALL ON geometry_columns TO PUBLIC;
template_postgis=# GRANT ALL ON geography_columns TO PUBLIC;
template_postgis=# GRANT ALL ON spatial_ref_sys TO PUBLIC;

Distance Calculations

Once this is done, you can add a distance attribute to Bands that have a latitude longitude. In this example I'm working out the distance of the bands from a fixed point, the band with id 1's location.

lOrigin = Band.objects.filter(id=1)[0]
try:
    lMatchingBand = Band.objects.filter(slug=pBandSlug)
    if lMatchingBand[0].latitude:
        lBand = lMatchingBand.distance(lOrigin.point)[0]
    else:   
        lBand = lMatchingBand[0]
except IndexError:
    raise Http404()

Notice that we decorate the band object with distance only if latitude is populated, otherwise we don't bother.