1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

gis: spatial-database compatibility and usability changes:

(1) The SpatialRefSys and GeometryColumns models and GeometryProxy have been moved to the PostGIS backend because their functionality depends on the spatial databse.
 (2) The GeoMixin is no longer required, as all the functionality contributed by the extra instance methods has been moved to the GeometryProxy.
 (3) The `_post_create_sql` field now returns a tuple of SQL statements, instead of a string.


git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6467 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2007-10-08 18:18:17 +00:00
parent 5d7fd63423
commit b6c8bba5b6
12 changed files with 251 additions and 199 deletions

View File

@ -1,15 +1,21 @@
"""
This module provides the backend for spatial SQL construction with Django.
This module provides the backend for spatial SQL construction with Django.
Specifically, this module will import the correct routines and modules
needed for GeoDjango
Specifically, this module will import the correct routines and modules
needed for GeoDjango.
(1) GeoBackEndField, a base class needed for GeometryField.
(2) The parse_lookup() function, used for spatial SQL construction by
the GeoQuerySet.
(1) GeoBackEndField, a base class needed for GeometryField.
(2) GeometryProxy, for lazy-instantiated geometries from the
database output.
(3) GIS_TERMS, a list of acceptable geographic lookup types for
the backend.
(4) The `parse_lookup` function, used for spatial SQL construction by
the GeoQuerySet.
(5) The `create_spatial_db`, `geo_quotename`, and `get_geo_where_clause`
routines (needed by `parse_lookup`.
Currently only PostGIS is supported, but someday backends will be added for
additional spatial databases (e.g., Oracle, DB2).
Currently only PostGIS is supported, but someday backends will be added for
additional spatial databases (e.g., Oracle, DB2).
"""
from django.conf import settings
from django.db import connection
@ -21,9 +27,11 @@ from django.utils.datastructures import SortedDict
ASGML, ASKML, UNION = (False, False, False)
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# PostGIS is the spatial database, getting the rquired modules, renaming as necessary.
# PostGIS is the spatial database, getting the rquired modules,
# renaming as necessary.
from django.contrib.gis.db.backend.postgis import \
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
PostGISProxy as GeometryProxy, \
create_spatial_db, geo_quotename, get_geo_where_clause, \
ASGML, ASKML, UNION
else:

View File

@ -7,6 +7,7 @@ from django.contrib.gis.db.backend.postgis.query import \
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
from django.contrib.gis.db.backend.postgis.field import PostGISField
from django.contrib.gis.db.backend.postgis.proxy import PostGISProxy
# Functions used by GeoManager methods, and not via lookup types.
if MAJOR_VERSION == 1:

View File

@ -44,9 +44,11 @@ class PostGISField(Field):
return sql
def _post_create_sql(self, style, db_table):
"""Returns SQL that will be executed after the model has been
"""
Returns SQL that will be executed after the model has been
created. Geometry columns must be added after creation with the
PostGIS AddGeometryColumn() function."""
PostGIS AddGeometryColumn() function.
"""
# Getting the AddGeometryColumn() SQL necessary to create a PostGIS
# geometry field.
@ -54,9 +56,9 @@ class PostGISField(Field):
# If the user wants to index this data, then get the indexing SQL as well.
if self._index:
return '%s\n%s' % (post_sql, self._geom_index(style, db_table))
return (post_sql, self._geom_index(style, db_table))
else:
return post_sql
return (post_sql,)
def _post_delete_sql(self, style, db_table):
"Drops the geometry column."
@ -66,9 +68,18 @@ class PostGISField(Field):
style.SQL_FIELD(quotename(self.column)) + ');'
return sql
def db_type(self):
"""
PostGIS geometry columns are added by stored procedures, should be
None.
"""
return None
def get_db_prep_lookup(self, lookup_type, value):
"""Returns field's value prepared for database lookup, accepts WKT and
GEOS Geometries for the value."""
"""
Returns field's value prepared for database lookup, accepts WKT and
GEOS Geometries for the value.
"""
if lookup_type in POSTGIS_TERMS:
if lookup_type == 'isnull': return [value] # special case for NULL geometries.
if not bool(value): return [None] # If invalid value passed in.
@ -101,6 +112,12 @@ class PostGISField(Field):
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
def get_internal_type(self):
"""
Returns NoField because a stored procedure is used by PostGIS to create the
"""
return 'NoField'
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the

View File

@ -0,0 +1,54 @@
"""
The GeometryColumns and SpatialRefSys models for the PostGIS backend.
"""
from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
# Checking for the presence of GDAL (needed for the SpatialReference object)
from django.contrib.gis.gdal import HAS_GDAL
if HAS_GDAL:
from django.contrib.gis.gdal import SpatialReference
class GeometryColumns(models.Model):
"""
The 'geometry_columns' table from the PostGIS. See the PostGIS
documentation at Ch. 4.2.2.
"""
f_table_catalog = models.CharField(maxlength=256)
f_table_schema = models.CharField(maxlength=256)
f_table_name = models.CharField(maxlength=256, primary_key=True)
f_geometry_column = models.CharField(maxlength=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField()
type = models.CharField(maxlength=30)
class Meta:
db_table = 'geometry_columns'
@classmethod
def table_name(self):
"Class method for returning the table name field for this model."
return 'f_table_name'
def __unicode__(self):
return "%s.%s - %dD %s field (SRID: %d)" % \
(self.f_table_name, self.f_geometry_column,
self.coord_dimension, self.type, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
"""
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
documentaiton at Ch. 4.2.1.
"""
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(maxlength=256)
auth_srid = models.IntegerField()
srtext = models.CharField(maxlength=2048)
proj4text = models.CharField(maxlength=2048)
class Meta:
db_table = 'spatial_ref_sys'
@property
def wkt(self):
return self.srtext

View File

@ -0,0 +1,62 @@
"""
The GeometryProxy object, allows for lazy-geometries. The proxy uses
Python descriptors for instantiating and setting GEOS Geometry objects
corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322).
"""
from types import NoneType, StringType, UnicodeType
class PostGISProxy(object):
def __init__(self, klass, field):
"""
Proxy initializes on the given Geometry class (not an instance) and
the GeometryField.
"""
self._field = field
self._klass = klass
def __get__(self, obj, type=None):
"""
This accessor retrieves the geometry, initializing it using the geometry
class specified during initialization and the HEXEWKB value of the field.
Currently, only GEOS or OGR geometries are supported.
"""
# Getting the value of the field.
geom_value = obj.__dict__[self._field.attname]
if isinstance(geom_value, self._klass):
geom = geom_value
elif (geom_value is None) or (geom_value==''):
geom = None
else:
# Otherwise, a GEOSGeometry object is built using the field's contents,
# and the model's corresponding attribute is set.
geom = self._klass(geom_value)
setattr(obj, self._field.attname, geom)
return geom
def __set__(self, obj, value):
"""
This accessor sets the proxied geometry with the geometry class
specified during initialization. Values of None, HEXEWKB, or WKT may
be used to set the geometry as well.
"""
# The OGC Geometry type of the field.
gtype = self._field._geom
# The geometry type must match that of the field -- unless the
# general GeometryField is used.
if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
# Assigning the SRID to the geometry.
if value.srid is None: value.srid = self._field._srid
elif isinstance(value, (NoneType, StringType, UnicodeType)):
# Set with None, WKT, or HEX
pass
else:
raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value)))
# Setting the objects dictionary with the value, and returning.
obj.__dict__[self._field.attname] = value
return value

View File

@ -1,9 +1,9 @@
from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend.
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.conf import settings
from django.contrib.gis.db.backend import GeoBackendField, GeometryProxy # these depend on the spatial database backend.
from django.contrib.gis.oldforms import WKTField
from django.utils.functional import curry
from django.contrib.gis.geos import GEOSGeometry
#TODO: Flesh out widgets.
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
class GeometryField(GeoBackendField):
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
@ -31,25 +31,8 @@ class GeometryField(GeoBackendField):
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)
# setup for lazy-instantiated GEOSGeometry objects
setattr(cls, self.attname, GeometryProxy(self))
# Adding needed accessor functions
setattr(cls, 'get_%s_geos' % self.name, curry(cls._get_GEOM_geos, field=self))
setattr(cls, 'get_%s_ogr' % self.name, curry(cls._get_GEOM_ogr, field=self, srid=self._srid))
setattr(cls, 'get_%s_srid' % self.name, curry(cls._get_GEOM_srid, srid=self._srid))
setattr(cls, 'get_%s_srs' % self.name, curry(cls._get_GEOM_srs, srid=self._srid))
setattr(cls, 'get_%s_wkt' % self.name, curry(cls._get_GEOM_wkt, field=self))
setattr(cls, 'get_%s_centroid' % self.name, curry(cls._get_GEOM_centroid, field=self))
setattr(cls, 'get_%s_area' % self.name, curry(cls._get_GEOM_area, field=self))
def get_internal_type(self):
return "NoField"
def db_type(self):
# Geometry columns are added by stored procedures, and thus should
# be None.
return None
# Setup for lazy-instantiated GEOSGeometry object.
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
def get_manipulator_field_objs(self):
"Using the WKTField (defined above) to be our manipulator."

View File

@ -1,54 +1,11 @@
from warnings import warn
# GEOS is a requirement, GDAL is not
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.gdal import HAS_GDAL
if HAS_GDAL:
from django.contrib.gis.gdal import OGRGeometry, SpatialReference
# Until model subclassing is a possibility, a mixin class is used to add
# the necessary functions that may be contributed for geographic objects.
class GeoMixin:
"The Geographic Mixin class provides routines for geographic objects."
# A subclass of Model is specifically needed so that these geographic
# routines are present for instantiations of the models.
def _get_GEOM_geos(self, field):
"Returns a GEOS Python object for the geometry."
warn("use model.%s" % field.attname, DeprecationWarning)
return getattr(self, field.attname)
def _get_GEOM_ogr(self, field, srid):
"Returns an OGR Python object for the geometry."
if HAS_GDAL:
return OGRGeometry(getattr(self, field.attname).wkt,
SpatialReference('EPSG:%d' % srid))
else:
raise Exception, "GDAL is not installed!"
def _get_GEOM_srid(self, srid):
"Returns the spatial reference identifier (SRID) of the geometry."
warn("use model.geometry_field.srid", DeprecationWarning)
return srid
def _get_GEOM_srs(self, srid):
"Returns ane OGR Spatial Reference object of the geometry."
if HAS_GDAL:
return SpatialReference('EPSG:%d' % srid)
else:
raise Exception, "GDAL is not installed!"
def _get_GEOM_wkt(self, field):
"Returns the WKT of the geometry."
warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning)
return getattr(self, field.attname).wkt
def _get_GEOM_centroid(self, field):
"Returns the centroid of the geometry, in WKT."
warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning)
return getattr(self, field.attname).centroid.wkt
def _get_GEOM_area(self, field):
"Returns the area of the geometry, in projected units."
warn("use model.%s.area" % field.attname, DeprecationWarning)
return getattr(self, field.attname).area
"""
The Geographic Mixin class provides routines for geographic objects,
however, it is no longer necessary, since all of its previous functions
may now be accessed via the GeometryProxy. This mixin is only provided
for backwards-compatibility purposes, and will be eventually removed
(unless the need arises again).
"""
pass

View File

@ -1,47 +0,0 @@
"""
The GeometryProxy object, allows for lazy-geometries. The proxy uses
Python descriptors for instantiating and setting GEOS Geometry objects
corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322).
"""
from types import NoneType, StringType, UnicodeType
from django.contrib.gis.geos import GEOSGeometry, GEOSException
# TODO: docstrings
class GeometryProxy(object):
def __init__(self, field):
"Proxy initializes on the given GeometryField."
self._field = field
def __get__(self, obj, type=None):
# Getting the value of the field.
geom_value = obj.__dict__[self._field.attname]
if isinstance(geom_value, GEOSGeometry):
# If the value of the field is None, or is already a GEOS Geometry
# no more work is needed.
geom = geom_value
elif (geom_value is None) or (geom_value==''):
geom = None
else:
# Otherwise, a GEOSGeometry object is built using the field's contents,
# and the model's corresponding attribute is set.
geom = GEOSGeometry(geom_value)
setattr(obj, self._field.attname, geom)
return geom
def __set__(self, obj, value):
if isinstance(value, GEOSGeometry) and (value.geom_type.upper() == self._field._geom):
# Getting set with GEOS Geometry; geom_type must match that of the field.
# If value's SRID is not set, setting it to the field's SRID.
if value.srid is None: value.srid = self._field._srid
elif isinstance(value, (NoneType, StringType, UnicodeType)):
# Getting set with None, WKT, or HEX
pass
else:
raise TypeError, 'cannot set %s GeometryProxy with value of type: %s' % (self._field._geom, type(value))
obj.__dict__[self._field.attname] = value
return value

View File

@ -1,8 +1,9 @@
"""
Models for the PostGIS/OGC database tables.
Imports the SpatialRefSys and GeometryColumns models dependent on the
spatial database backend.
"""
import re
from django.db import models
from django.conf import settings
# Checking for the presence of GDAL (needed for the SpatialReference object)
from django.contrib.gis.gdal import HAS_GDAL
@ -15,37 +16,11 @@ if HAS_GDAL:
# parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
# This is the global 'geometry_columns' from PostGIS.
# See PostGIS Documentation at Ch. 4.2.2
class GeometryColumns(models.Model):
f_table_catalog = models.CharField(maxlength=256)
f_table_schema = models.CharField(maxlength=256)
f_table_name = models.CharField(maxlength=256, primary_key=True)
f_geometry_column = models.CharField(maxlength=256)
coord_dimension = models.IntegerField()
srid = models.IntegerField()
type = models.CharField(maxlength=30)
class Meta:
db_table = 'geometry_columns'
def __str__(self):
return "%s.%s - %dD %s field (SRID: %d)" % \
(self.f_table_name, self.f_geometry_column,
self.coord_dimension, self.type, self.srid)
# This is the global 'spatial_ref_sys' table from PostGIS.
# See PostGIS Documentation at Ch. 4.2.1
class SpatialRefSys(models.Model):
srid = models.IntegerField(primary_key=True)
auth_name = models.CharField(maxlength=256)
auth_srid = models.IntegerField()
srtext = models.CharField(maxlength=2048)
proj4text = models.CharField(maxlength=2048)
class Meta:
db_table = 'spatial_ref_sys'
class SpatialRefSysMixin(object):
"""
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
@property
def srs(self):
"""
@ -58,23 +33,16 @@ class SpatialRefSys(models.Model):
else:
# Attempting to cache a SpatialReference object.
# Trying to get from WKT first
# Trying to get from WKT first.
try:
self._srs = SpatialReference(self.srtext, 'wkt')
return self._srs.clone()
except Exception, msg1:
self._srs = SpatialReference(self.wkt)
return self.srs
except Exception, msg:
pass
# Trying the proj4 text next
try:
self._srs = SpatialReference(self.proj4text, 'proj4')
return self._srs.clone()
except Exception, msg2:
pass
raise Exception, 'Could not get an OSR Spatial Reference:\n\tWKT error: %s\n\tPROJ.4 error: %s' % (msg1, msg2)
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
else:
raise Exception, 'GDAL is not installed!'
raise Exception('GDAL is not installed.')
@property
def ellipsoid(self):
@ -85,7 +53,7 @@ class SpatialRefSys(models.Model):
if HAS_GDAL:
return self.srs.ellipsoid
else:
m = spheroid_regex.match(self.srtext)
m = spheroid_regex.match(self.wkt)
if m: return (float(m.group('major')), float(m.group('flattening')))
else: return None
@ -139,10 +107,18 @@ class SpatialRefSys(models.Model):
"Returns the name of the angular units."
return self.srs.angular_name
def __str__(self):
def __unicode__(self):
"""
Returns the string representation. If GDAL is installed,
it will be 'pretty' OGC WKT.
"""
if HAS_GDAL: return str(self.srs)
else: return "%d:%s " % (self.srid, self.auth_name)
try:
return unicode(self.srs)
except:
return unicode(self.srtext)
# The SpatialRefSys and GeometryColumns models
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
else:
raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE)

View File

@ -1,16 +1,21 @@
from django.contrib.gis.db import models
class Country(models.Model, models.GeoMixin):
name = models.CharField(maxlength=30)
class Country(models.Model):
name = models.CharField(max_length=30)
mpoly = models.MultiPolygonField() # SRID, by default, is 4326
objects = models.GeoManager()
class City(models.Model, models.GeoMixin):
name = models.CharField(maxlength=30)
class City(models.Model):
name = models.CharField(max_length=30)
point = models.PointField()
objects = models.GeoManager()
class State(models.Model, models.GeoMixin):
name = models.CharField(maxlength=30)
class State(models.Model):
name = models.CharField(max_length=30)
poly = models.PolygonField(null=True) # Allowing NULL geometries here.
objects = models.GeoManager()
class Feature(models.Model):
name = models.CharField(max_length=20)
geom = models.GeometryField()
objects = models.GeoManager()

View File

@ -1,6 +1,7 @@
import unittest
from models import Country, City, State
from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon
from models import Country, City, State, Feature
from django.contrib.gis.geos import *
from django.contrib.gis import gdal
class GeoModelTest(unittest.TestCase):
@ -59,7 +60,16 @@ class GeoModelTest(unittest.TestCase):
nullstate = State(name='NullState', poly=ply)
self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None
nullstate.save()
self.assertEqual(ply, State.objects.get(name='NullState').poly)
ns = State.objects.get(name='NullState')
self.assertEqual(ply, ns.poly)
# Testing the `ogr` and `srs` lazy-geometry properties.
if gdal.HAS_GDAL:
self.assertEqual(True, isinstance(ns.poly.ogr, gdal.OGRGeometry))
self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb)
self.assertEqual(True, isinstance(ns.poly.srs, gdal.SpatialReference))
self.assertEqual('WGS 84', ns.poly.srs.name)
# Changing the interior ring on the poly attribute.
new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30))
@ -278,6 +288,32 @@ class GeoModelTest(unittest.TestCase):
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.union('point'))
def test18_geometryfield(self):
"Testing GeometryField."
f1 = Feature(name='Point', geom=Point(1, 1))
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
f4 = Feature(name='GeometryCollection',
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))))
f1.save()
f2.save()
f3.save()
f4.save()
f_1 = Feature.objects.get(name='Point')
self.assertEqual(True, isinstance(f_1.geom, Point))
self.assertEqual((1.0, 1.0), f_1.geom.tuple)
f_2 = Feature.objects.get(name='LineString')
self.assertEqual(True, isinstance(f_2.geom, LineString))
self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple)
f_3 = Feature.objects.get(name='Polygon')
self.assertEqual(True, isinstance(f_3.geom, Polygon))
f_4 = Feature.objects.get(name='GeometryCollection')
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
self.assertEqual(f_3.geom, f_4.geom[2])
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(GeoModelTest))

View File

@ -404,7 +404,7 @@ def custom_sql_for_model(model, style):
# Post-creation SQL should come before any initial SQL data is loaded.
for f in opts.fields:
if hasattr(f, '_post_create_sql'):
output.append(f._post_create_sql(style, model._meta.db_table))
output.extend(f._post_create_sql(style, model._meta.db_table))
# Some backends can't execute more than one SQL statement at a time,
# so split into separate statements.