|
FOSS4GWorkshop
GeoDjango Workshop, FOSS4G 2008Presented by Josh Livni and Christopher Schmidt Document contributions from Tim Sutton and Tyler Erickson TOPICS
1. INTRODUCTIONNOTE: This document has been modified from the actual workshop given at FOSS4G. For example, installation instructions have been removed (as they will soon be deprecated by those available at geodjango.org). Various other modifications have also taken place. Code for this project may be downloaded from http://geodjango-basic-apps.googlecode.com/svn/trunk/projects/cape/ According to django website (djangoproject.com), Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. For projects where you need to get a robust database driven application online in a timely manner, Django can be an exceptional tool. In line with another popular Django slogan, GeoDjango has been called a Geographic Web Framework for Perfectionists with Deadlines. As of Django v1.0, GeoDjango is part of the core set of Django packages that ship with the default download. Adding Geographic capabilities to a Django project is as simple as ensuring you have the base software stack, and enabling the gis contrib package within Django. This workshop is designed for people who may be new to Django, but have familiarity with python and general web concepts, such as HTML/CSS, templating, and apache or other web servers. Although it is not required, you will likely get more out of this workshop if you try make it through the introductory Django tutorial at djangoproject.com before the FOSS4G conference.
2: INSTALLATIONGeoDjango can work with different spatial databases, and can function without GDAL/OGR. However, for the purposes of this workshop we will be using PostGIS and GDAL/OGR. 2.1: VMWare Virtual Machine (Recommended): A VMWare image will be provided to workshop participants on DVD at the start of the workshop. The VMWare image has all the required software pre-installed on a linux base, including:
Under the assumption we may not have network access, the VMWare image also includes a WMS server that can be used by OpenLayers to display some basic administrative boundaries as a background to the data we'll be interacting with. VMWARE REQUIREMENTS:
2.3: Self Installation: REMOVED 2.4 Test your installation Before we get started with Django, let's be sure we have all the software correctly installed.
from django.contrib.gis.gdal import HAS_GDAL HAS_GDAL You should get a result of 'True'. If not, something is amiss with your GDAL installation. 3: STARTING YOUR GEODJANGO PROJECTFor this workshop, we will be creating a project called "cape", a simple database driven web application that lets users add and edit geospatial data via a map interface. NOTE: A django 'project' can contain many django 'applications', which can exist simply as code inside subfolders within your project folder or a folder anywhere on your PYTHONPATH. We will be creating both a project and an application from scratch, and we will also include some other applications to bring in some features for us that we don't want to build ourselves. One nice thing about this is by decoupling our application from a particular website/project, we can reuse the generic application we're building today in any GeoDjango website with just a few simple lines of code saying to include it.
django-admin.py startproject cape This will create a folder called cape with some basic python scripts to initiate a django project, including a manage.py script that can be run to do various django tasks and a settings.py file (see below). Note: If django-admin is not in your path you may need to preface the command with the full path of the django binaries, (eg c:\python25\lib\site-packages\django\bin\ on windows)
DATABASE_ENGINE = 'postgresql_psycopg2'
DATABASE_NAME = 'cape'
DATABASE_USER = 'postgres'
#scroll down to see this
INSTALLED_APPS = (
#...don't delete what's already here
'django.contrib.admin',
'django.contrib.gis',
)Note: you will also need to set the DATABASE_PASSWORD to the password for your postgres user if it has one.
createdb -T postgis -U postgres cape python manage.py syncdb
python manage.py runserver
from django.contrib import admin admin.autodiscover() # ... (r'^admin/(. *)', admin.site.root),
4: CREATING YOUR APPLICATIONWe're now going to import some administrative boundaries from a shapefile into GeoDjango. Open up the geodjango python shell (python manage.py shell in the command prompt) to start it up again if you need, and inspect the Shapefile: python manage.py startapp lionshead Django has now created some more boilerplate for us, including some empty files that we'll be modifying to add functionality. Note how you now have a new folder called lionshead, and inside the folder are 3 new files. The next step is to tell Django to include this new application, like we did with some of the contributed ones above. Modify your settings.py so the INSTALLED_APPS tuple includes 'lionshead', 4.1 CREATING YOUR CUSTOM MODELS In django lingo, we will have a variety of "models", each of which relates to a spatially enabled table in the PostGIS database. To get an idea of some of the things geodjango offers us, we will be both creating the models by hand, and by auto-generating them from an existing shapefile. Now is the time to break into teams, so you can help eachother out as you work to get your first models in this application up and running. For the first phase, we will be creating models manually. Each team will create the first two models by hand, and ensure they are able to work with the geographic data in these models using the default GeoDjango admin panel. For this application, we're going to build a model for recording Locations of Interest (points), and a model for storing election wards (polygons). Open up your models.py, and let's create our first model. Your models.py should look like this: from django.contrib.gis.db import models
class InterestingLocation(models.Model):
"""A spatial model for interesting locations """
name = models.CharField(max_length=50, )
description = models.TextField()
interestingness = models.IntegerField()
geometry = models.PointField(srid=4326) #EPSG:4236 is the spatial reference for our data
objects = models.GeoManager() # so we can use spatial queryset methods
def __unicode__(self): return self.name Because we've added the lionshead application to our INSTALLED_APPS for our project, and created a new model in the lionshead application, we can now run the syncdb command again to have django create the appropriate table for us in our PostGIS database: python manage.py syncdb 5: MODIFYING SOME ACTUAL DATA5.1 WITH THE PYTHON SHELL
python manage.py shell
from django.contrib.gis.utils import add_postgis_srs add_postgis_srs(900913)
from lionshead.models import InterestingLocation from django.contrib.gis.geos import Point il = InterestingLocation() il.name = 'some place' il.interestingness = 3 il.geometry = Point(-16.57,14.0) il.save()
5.2 IN THE ADMIN At this point, we could start building some views and templates that we will need in order to actually display data from this model in our public facing site. However, since we're lazy (for now), let's register this model with the admin, and then we can use it to modify and view our data. First create a new file called "admin.py" in the lionshead folder, and add these import lines to the top: from django.contrib.gis import admin from models import * Below the imports, create a new OSMGeoAdmin class, and register the model with the admin package. The OSMGeoAdmin uses OpenStreetMap data for the background, and displays points in a 'spherical mercator' projection. class InterestingLocationAdmin(admin.OSMGeoAdmin):
list_display = ('name','interestingness')
list_filter = ('name','interestingness',)
fieldsets = (
('Location Attributes', {'fields': (('name','interestingness'))}),
('Editable Map View', {'fields': ('geometry',)}),
)
# Default GeoDjango OpenLayers map options
scrollable = False
map_width = 700
map_height = 325# Register our model and admin options with the admin site admin.site.register(InterestingLocation, InterestingLocationAdmin) Ok - now, assuming no typos, we should be able to reload http://localhost:8000/admin in the browser and see our new Locations model. Click on it, and then at the top right click 'Add Interesting Location' to add a new Interesting Location. You will need to fill out the name and interestingness values (by default they are required, and we did not specify they are optional), and then you can click the map to add a new location, and save. At this point, we have covered one of the key functionalities of GeoDjango and of building yourself a GIS enabled website: An interactive map that lets you view/modify spatial data that's stored inside PostGIS. To summarize what we've done so far:
6: BUILDING A PUBLIC WEBSITEThe next step we'll do is customize some views and associated templates which will be the public facing website. To display our map publicly, we are going to build two basic items:
6.1: OUR FIRST VIEW
from django.shortcuts import render_to_response
from django.contrib.gis.shortcuts import render_to_kml
from lionshead.models import *
def all_kml(request):
locations = InterestingLocation.objects.kml()
return render_to_kml("placemarks.kml", {'places' : locations}) Pretty simple, eh? As you can see, we've imported a few things: 6.2 OUR FIRST TEMPLATES Next, we need to make that placemarks.kml. Oh wait - actually we don't, since a basic templates.kml is already included for us with the GeoDjango installation. You can go see it at django/contrib/gis/templates/gis/kml Since Django knows about our contrib.gis package, and it's associated templates, we can use the placemarks.kml by telling our view where this specific template is relative to our base gis templates folder. Change the render_to_kml in the view we made from "placemarks.kml" to the relative path from our gis contrib templates: "gis/kml/placemarks.kml"/ NOTE: If we wanted to customize these (which we will later), we could simply copy them to our own 'templates' subfolder, and they would take precedence over the default contrib templates, but for now these generic ones will do. At this point, we should just be able to access our view and be good to go. But how do we access it? 6.3 URLS We still haven't given Django enough information to know how to direct users to a particular page. This is where the urls.py comes in - it controls which views get loaded based on what URL a user is requesting. Open up the urls.py and add the following import statement: from lionshead.views import * And then, add this line under the admin tuple: (r'^kml/', all_kml) This URL pattern says that if someone goes to http://yoursite/kml ,django will run the all_kml view, and do whatever it says to do (in our case, return some kml placemarks) Now load up http://localhost:8000/kml 6.4 THE PUBLIC MAP PAGE Not only did GeoDjango automagically set the appropriate KML mime-type for us, but with just a 3 line view and a simple url pattern, we have a nice kml showing all our geodata. Feel free to add a new location or two via the Admin site, and reload the kml. Now that we have a working kml feed, we can add it to a map. But first we need a map to add it to, and for this we're going to create a new view and associated template that will display an openlayers map for the public: Back in your views.py, add the following method: def map_page(request):
lcount = InterestingLocation.objects.all().count()
return render_to_response('map.html', {'location_count' : lcount}) This view says that it will load a template called "map.html", and that template will get passed the total number of location objects we have in the database. Let's create that template: In your lionshead folder, create a folder called 'templates', and within this folder, create a new file called 'map.html'. This file should look something like this: <html>
<head>
<script src="http://openlayers.org/api/OpenLayers.js"></script>
<script type="text/javascript">
var map, base_layer, kml;
function init(){
map = new OpenLayers.Map('map');
base_layer = new OpenLayers.Layer.WMS( "OpenLayers WMS",
"http://labs.metacarta.com/wms/vmap0", {layers: 'basic'} );
kml = new OpenLayers.Layer.GML("KML", "/kml/",
{ format: OpenLayers.Format.KML });
map.addLayers([base_layer, kml]);
map.zoomToMaxExtent();
}
</script>
</head>
<body onload="init()">
Hi. There should be a total of {{location_count}} Locations.<br />
<div id="map"></div>
</body>
</html>And again, we need to create a url mapping so we can get to this view. In urls.py, add: (r'^$', map_page) We've now mapped the root of the site (/) to the index view we created, which in turn will display the map.html with the location_count variable dynamically filled in by Django. This combination of a view and a template (along with an associated url mapping) are how we'll build most of the public facing pages. There are a variety of shortcuts that Django offers, letting you entirely skip the creation of views for common view tasks, and letting you build your views in a generic manner so they can be reused by multiple urls and templates, but all that is out of the scope of this workshop. If we wanted to provide an alternative download form for users who wanted to open the data in something like (for example) Google Earth, we could add a regular HTML link to the /kml/ URL: <body onload="init()">
Hi. There should be a total of {{location_count}} Locations.<br />
To see this data in Google Earth, <a href="/kml/">Open as KML</a>.
<div id="map"></div>
</body>7: IMPORTING FROM A SHAPEFILEWe're now going to import some administrative boundaries from a shapefile into GeoDjango. Open up the geodjango python shell (python manage.py shell in the command prompt to start it up again if you need , and inspect the Shapefile: from django.contrib.gis.gdal import DataSource
wards = DataSource('data/wards_4326.shp')
layer = wards[0]
print layer.fields
print len(layer) # getting the number of features in the layer
print layer.geom_type.name # Should a polygon
print layer.srs.name # WGS84Now that we know what attributes the shapefile has, we can create a model for it. Open up your models.py in your text editor, and add a new model: class Ward(models.Model):
"""Spatial model for Cape Town Wards"""
subcouncil = models.CharField(max_length=20)
party = models.CharField(max_length=50)
ward = models.CharField(max_length=50)
cllr = models.CharField(max_length=150)
geometry = models.MultiPolygonField(srid=4326)
objects = models.GeoManager()
def __unicode__(self): return '%s (%s)' % (self.cllr, self.party) Now that the Model is created, we can run syncdb to have django create us the associated table in the database. At the command line, run: python manage.py syncdb And, back in our python shell, we can define the Layer Mapping and import the code from lionshead.models import Ward from django.contrib.gis.utils import mapping, LayerMapping print mapping(wards) lm = LayerMapping(Ward, wards, mapping(wards, geom_name='geometry')) lm.save(verbose=True) This will read the data from the provinces shapefile and load it into our Ward model in PostGIS. As before, we need to hook this up into our Admin, so open up admin.py and add a new class: class WardAdmin(admin.OSMGeoAdmin):
list_display = ('cllr','ward')
fieldsets = (
('Location Attributes', {'fields': (('cllr','ward'))}),
('Editable Map View', {'fields': ('geometry',)}),
)
scrollable = False
map_srid = 4326
debug = True
# Register our model and admin options with the admin site
admin.site.register(InterestingLocation, InterestingLocationAdmin)
admin.site.register(Ward, WardAdmin) NOTE: We could also add more openlayers options to the admin. For example, we can give a different URL for the OpenLayers library, as follows: openlayers_url = '/static/openlayers/lib/OpenLayers/js'In this case, we would need to also modify our urls.py so that /static would point to a local location for our static files: (r'^static/(?P<path>. *)$', 'django.views.static.serve', {
'document_root': 'q:\projects\cape\static', 'show_indexes': True}),For a larger list of options we can pass to the map admin, see http://code.djangoproject.com/browser/django/trunk/django/contrib/gis/admin/options.py 8: SOME ACTUAL GISOnce we have done this, we could certainly create a similar KML output to the InterestingPoint KML we created above for all the Ward geometries. But as much fun as displaying one data layer at a time can be, let's instead create a more complex view that displays just one ward (which will be specified in the URL), and have this view feed a map template that highlights the appropriate ward, and all the Interesting Locations that are within 5 miles of that ward. In this section, we'll be creating a ward-specific viewing page. That ward page will list the interesting points inside the ward, and list them in the HTML page, and finally display a map. First, we'll edit the urls.py file. Earlier, we built a urls.py that had in it: (r'^kml/', all_kml), (r'^/$', map_page) Now, we'll be adding one more URL to this list: (r'^kml/', all_kml), (r'^/$', map_page), (r'^wards/(?P<id>[0-9] *)/', ward), We've added an item for viewing a specific ward. You'll see here that there is additional syntax in this URL: This URL matching is a regular expression, and allows us to pull the ID from the URL and use it as a parameter to the ward function we're going to write. There are two different queries we want to perform in order to build the ward page. First, we want to take the passed in ID, and we want to find the associated ward. If the ID is invalid, we want to return a 404 to the user. Django has a shortcut for this type of thing, because it's a relatively common operation. Like all shortcuts, it is in django.shortcuts, and it's called 'get_object_or_404'. We'll use this to get our ward: from django.shortcuts import get_object_or_404
...
def ward(request, id):
ward = get_object_or_404(Ward, pk=id)Using this, we get the ward we care about as the ward variable. Then, we'll want to find nearby interesting points -- within 5 miles of the borders of our ward. NOTE xxxxxx Change below to actually use a 5mile buffer xxxxxxxxx def ward(request, id):
ward = get_object_or_404(Ward, pk=id)
interesting_points = InterestingLocation.objects.filter(
geometry__intersects=(ward.geometry)) Then, we'll pass both of these objects -- the ward, and the list of locations -- into the ward template. def ward(request, id):
ward = get_object_or_404(Ward, pk=id)
interesting_points = InterestingLocation.objects.filter(
geometry__intersects=(ward.geometry))
return render_to_response("ward.html", {
'ward': ward, 'interesting_points': interesting_points }) Next, we'll build our template. Because we'll be building a map again, we can start with the same template as we use for our map_page view, so start by copying 'map.html' to 'ward.html' in your lionshead/templates directory. Now, instead of loading data from the KML view we created earlier, we're going to create features directly in our template. You could also create a second view to serve your data up to be loaded asynchronously (as we did with the KML before): this is just demonstrating another way to achieve the same goal. So, remove the 'kml = new OpenLayers.Layer.GML' line and the line that follows it from the new 'ward.html' template. Next, you'll be creating a geometry from GeoJSON created in the template. geojson_format = new OpenLayers.Format.GeoJSON()
ward = geojson_format.read({{ ward.geometry.geojson|safe}})[0];
// We mark it 'safe' so that Django doesn't escape the quotes.
ward.attributes = { 'name': "{{ward.name}}", 'type': 'ward'};
vectors = new OpenLayers.Layer.Vector("Data");
vectors.addFeatures(ward);Next, change the 'kml' in the addLayers call to 'vectors'. Also, we only care about this ward at this time, so we can change the setCenter call to "map.zoomToExtent(ward.geometry.getBounds());" -- This will zoom the map to just the bounds of the ward. Finally, change the {{location_count}} variable in the text further down the page to simply {{interesting_points.count}} -- this will issue a SELECT COUNT(*) FROM query to the database, giving back a count of the number of matching features. Now, we should be able to loop over the points in the ward, and add each of them to the map as well. We also want to add them to the HTML page, so we'll do one iteration in the main body of the page, and add features to an array to be parsed in the loading function as we go. Directly above the we'll add the following code: <script> var points = []; </script>
<ul>
{% for point in interesting_points %}
<li>{{ point.name }} -- {{point.interestingness}}</li>
<script>points.push({{point.geometry.geojson|safe}});</script>
{% endfor %}
</ul> Now, each of your points will be displayed on the map. You can then add code to the initialize function to add them to your vector layer. This should go immediately after the 'vectors.addFeatures' call above. for (var i = 0; i < points.length; i++) {
point = format.read(points[i])[0];
point.attributes = {'type':'point'};
vectors.addFeatures(point);
} Now, you can visit your ward page by going to, for example, http://localhost:8000/wards/96/ . You can compare this to viewing in the admin by visiting http://localhost:8000/admin/lionshead/ward/96/ . Now, we can go to http://localhost:8000/admin/lionshead/interestinglocation/add/, and scroll to the Southeast of Cape Town. You will see a label that says "Strand". Place a marker in the residential area here, and then visit http://localhost:8000/wards/96/ again, and you should see a single interesting point listed in this area. Congratulations, you've just combined the data of two different layers in GeoDjango, using a database-backed intersection query. 9: MORE GEODJANGO FEATURESfrom django.contrib.gis.geos import *
from django.contrib.gis.measure import D # D is a shortcut for Distance
from lionshead import InterestingLocation, Ward
from world.models import Countries
#define a point location
pnt = fromstr('POINT(18.4 -33.9)', srid=4326)
OUTPUT FORMATS
pnt.geometry.kml
pnt.geometry.geojson
pnt.geometry.gml
BUFFERING
pnt.buffer(1.5).area
pnt.buffer(1).kml
DISTANCE QUERIES
# Distances will be calculated from this point, which does not have to be projected.
# If numeric parameter, units of field (meters in this case) are assumed.
#queryset of all interesting locations within 500 miles, 50km, or 100 chains of this point
qs=InterestingLocation.objects.filter(geometry__distance_lte=(pnt,D(mi=500)))
qs=InterestingLocation.objects.filter(geometry__distance_lte=(pnt,D(km=50)))
qs=InterestingLocation.objects.filter(geometry__distance_lte=(pnt,D(chain=100)))
poly = WorldBorders.objects.get(name='Algeria').geometry
for loc in InterestingLocation.objects.distance(poly.centroid): print loc.name, loc.distance.km
BOUNDING BOX
qs = Ward.objects.filter(name__in=('foo', 'bar'))
print qs.extent() 10: ENTERING DATA FROM THE PUBLIC SITEREMOVED 11: WORKSHOP TROUBLESHOOTINGIt is quite possible workshop attendees will not have internet access. In this case, the default OpenLayers map settings (which assume they will have access to live WMS or other tile services) will fail. We can fix this by:
wms_url = '/cgi-bin/mapserv?map=/home/user/wms/cape.map&'
wms_layer = 'countries,wards'
wms_name = 'Local WMS'
var ms_url = '/cgi-bin/mapserv?map=/home/user/wms/cape.map&'
var countries = new OpenLayers.Layer.WMS("Countries",
ms_url, {layers : 'countries'} );
var wards = new OpenLayers.Layer.WMS("Wards",
ms_url, {layers : 'wards'} );
map.addLayers([countries, wards, kml]);APPENDIX B: OS X INSTALLWilliam Kyngesburye provides a number of geo-related binary packages that can get you most of the way to having Django working and installed without compiling anything from source. From http://www.kyngchaos.com/wiki/software:frameworks :
From http://www.kyngchaos.com/wiki/doku.php?id=software:postgres :
After you have installed each of these, you'll need the psycopg2 bindings. I could not find a working version of these for OS X, so I did: sudo su PATH="$PATH:/usr/local/pgsql/bin" python easy_install psycopg2 This requires the OS X Developer tools to be installed. Then, just download and install Django itself, and you're all set. If you get a complaint about GEOS_LIBRARY_PATH not being set, add the following to your settings file: GEOS_LIBRARY_PATH="/Library/Frameworks/GEOS.framework/unix/lib/libgeos_c.dylib" |
Sign in to add a comment
Rendering Polygons to KML
How do I use render_to_kml with a bunch of polygons instead of points?
I figure I must have a different kml template, since the ones included with geodjango (base.kml and placemarks.kml) won't work for that.
Is there anything else?
Hello, I am new to Django/Geodjango Your tutorial is great!
I just have a problem : when I load 'http://127.0.0.1:8000/kml/' to display the map with my locations, I get an error : "KML stored procedure not available." I think this is because I am using MySQL. Do you have any answer?
Thanks a lot
Romain
yes you need to change mysql to postgresql-psycopg2