Geographic Information Systems (GIS)
GeoDjango extends the Django ORM to support geographic data types, spatial lookups, and database functions. By using the spatial backends (PostGIS, SpatiaLite, MySQL, or Oracle), you can perform complex geographic queries like finding all points within a certain distance or determining if a point is contained within a polygon.
In this tutorial, you will build a simple spatial application to track cities and countries, perform spatial lookups, and calculate geographic properties like area and distance.
Prerequisites
To use the spatial features in this codebase, you must:
- Use a spatial database (e.g., PostGIS or SpatiaLite).
- Have the GEOS and GDAL libraries installed on your system.
- Add
'django.contrib.gis'to yourINSTALLED_APPS.
Step 1: Defining Spatial Models
The foundation of any GeoDjango application is the GeometryField. While you can use GeometryField directly, you typically use its more specific subclasses like PointField or MultiPolygonField.
Create a model for cities and countries in your models.py:
from django.contrib.gis.db import models
class Country(models.Model):
name = models.CharField(max_length=30)
# MultiPolygonField stores multiple polygons (e.g., for countries with islands)
# Default SRID is 4326 (WGS 84)
mpoly = models.MultiPolygonField()
def __str__(self):
return self.name
class City(models.Model):
name = models.CharField(max_length=30)
# PointField stores a single pair of coordinates
point = models.PointField()
# Use geography=True for high-accuracy calculations over large distances
# (PostGIS only)
location = models.PointField(geography=True, null=True)
def __str__(self):
return self.name
The GeometryField handles the underlying spatial database column. By default, it uses SRID 4326 (longitude/latitude). You can customize this using the srid parameter, as seen in tests/gis_tests/geoapp/models.py with CountryWebMercator using srid=3857.
Step 2: Inserting Spatial Data
To save spatial data, you use GEOSGeometry objects or their specific subclasses like Point.
from django.contrib.gis.geos import Point, MultiPolygon, Polygon
from .models import City, Country
# Create a City using a Point object (longitude, latitude)
houston = City.objects.create(
name="Houston",
point=Point(-95.363151, 29.763374)
)
# Create a Country using a MultiPolygon
# (Simplified example of a square polygon)
poly = Polygon(((0, 0), (0, 50), (50, 50), (50, 0), (0, 0)))
texas = Country.objects.create(
name="Texas",
mpoly=MultiPolygon(poly)
)
Step 3: Performing Spatial Lookups
GeoDjango provides GISLookup classes that allow you to use spatial operators in your filters. Common lookups include contains, intersects, and dwithin.
Finding Containment
To find which country contains a specific city:
houston = City.objects.get(name="Houston")
# The 'contains' lookup checks if the geometry contains the given point
tx = Country.objects.get(mpoly__contains=houston.point)
print(tx.name) # Output: Texas
Distance-based Queries
To find cities within a certain distance of a point, use the dwithin lookup combined with a Distance object (aliased as D):
from django.contrib.gis.measure import D
from django.contrib.gis.geos import Point
pnt = Point(-95.0, 30.0)
# Find cities within 5 kilometers of the point
nearby_cities = City.objects.filter(point__dwithin=(pnt, D(km=5)))
Note that dwithin is often more efficient than calculating exact distances because it can utilize spatial indexes.
Step 4: Using Geographic Functions
The GeoFunc class is the base for geographic database functions. These can be used to annotate your querysets with calculated values like area, distance, or transformed coordinates.
Calculating Area and Distance
You can use functions.Area and functions.Distance to add calculated fields to your results:
from django.contrib.gis.db.models import functions
# Annotate countries with their area
countries = Country.objects.annotate(area=functions.Area("mpoly"))
for c in countries:
print(f"{c.name}: {c.area.sq_m} sq meters")
# Calculate distance from a specific point to each city
pnt = Point(-95.36, 29.76, srid=4326)
cities = City.objects.annotate(distance=functions.Distance("point", pnt))
Coordinate Transformation
If you need to display coordinates in a different spatial reference system (e.g., for a specific map projection), use functions.Transform:
# Transform coordinates to Web Mercator (SRID 3857)
cities = City.objects.annotate(
transformed_point=functions.Transform("point", 3857)
)
print(cities[0].transformed_point.srid) # Output: 3857
Important Considerations
- SRID Matching: When performing lookups or functions, ensure your geometries have the same SRID. GeoDjango's
GeoFuncwill attempt automatic SRID conversion, but it is best practice to be explicit. - Bounding Box Lookups: Some lookups like
contained(different fromcontains) may only check the bounding box of the geometry depending on the backend. For example, intests/gis_tests/geoapp/tests.py, Oklahoma City is found to becontainedin the bounding box of Texas even though it is not inside the state boundaries. - Spatial Indexing: For performance, always ensure your spatial columns are indexed. Django's
migrationshandle this automatically forGeometryFieldsubclasses.