Skip to main content

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:

  1. Use a spatial database (e.g., PostGIS or SpatiaLite).
  2. Have the GEOS and GDAL libraries installed on your system.
  3. Add 'django.contrib.gis' to your INSTALLED_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 GeoFunc will attempt automatic SRID conversion, but it is best practice to be explicit.
  • Bounding Box Lookups: Some lookups like contained (different from contains) may only check the bounding box of the geometry depending on the backend. For example, in tests/gis_tests/geoapp/tests.py, Oklahoma City is found to be contained in 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 migrations handle this automatically for GeometryField subclasses.