Refactored and cleaned up parts of the spatial database backend. Changes include:

* Laid foundations for SpatiaLite support in `GeoQuerySet`, `GeoWhereNode` and the tests.
* Added the `Collect` aggregate for PostGIS (still needs tests).
* Oracle now goes to 11.
* The backend-specific `SpatialRefSys` and `GeometryColumns` models are now attributes of `SpatialBackend`.
* Renamed `GeometryField` attributes to be public that were private (e.g., `_srid` -> `srid` and `_geom` -> `geom_type`).
* Renamed `create_test_db` to `create_test_spatial_db`.
* Removed the legacy classes `GeoMixin` and `GeoQ`.
* Removed evil `\` from spatial backend fields.
* Moved shapefile data from `tests/layermap` to `tests/data`.

Fixed #9794.  Refs #9686.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@10197 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2009-03-30 17:15:49 +00:00
parent a61c0b7949
commit 867e71501c
47 changed files with 595 additions and 410 deletions

View File

@ -9,10 +9,12 @@ from django.contrib.gis.db.backend.util import gqn
# Retrieving the necessary settings from the backend.
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
from django.contrib.gis.db.backend.postgis import create_test_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
from django.contrib.gis.db.backend.oracle import create_test_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'mysql':
from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
from django.contrib.gis.db.backend.mysql import create_test_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'sqlite3':
from django.contrib.gis.db.backend.spatialite import create_test_spatial_db, get_geo_where_clause, SpatialBackend
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)

View File

@ -23,7 +23,4 @@ class BaseSpatialBackend(object):
return self.__dict__[name]
except KeyError:
return False

View File

@ -1,8 +1,8 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
from django.contrib.gis.db.backend.mysql.creation import create_test_spatial_db
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
from django.contrib.gis.db.backend.mysql.query import *

View File

@ -1,5 +1,5 @@
def create_spatial_db(test=True, verbosity=1, autoclobber=False):
if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py')
def create_test_spatial_db(verbosity=1, autoclobber=False):
"A wrapper over the MySQL `create_test_db` method."
from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber)

View File

@ -13,20 +13,20 @@ class MySQLGeoField(Field):
def _geom_index(self, style, db_table):
"""
Creates a spatial index for the geometry column. If MyISAM tables are
used an R-Tree index is created, otherwise a B-Tree index is created.
used an R-Tree index is created, otherwise a B-Tree index is created.
Thus, for best spatial performance, you should use MyISAM tables
(which do not support transactions). For more information, see Ch.
(which do not support transactions). For more information, see Ch.
16.6.1 of the MySQL 5.0 documentation.
"""
# Getting the index name.
idx_name = '%s_%s_id' % (db_table, self.column)
sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \
style.SQL_TABLE(qn(idx_name)) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + '(' + \
style.SQL_FIELD(qn(self.column)) + ');'
sql = (style.SQL_KEYWORD('CREATE SPATIAL INDEX ') +
style.SQL_TABLE(qn(idx_name)) +
style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + '(' +
style.SQL_FIELD(qn(self.column)) + ');')
return sql
def post_create_sql(self, style, db_table):
@ -35,19 +35,19 @@ class MySQLGeoField(Field):
created.
"""
# Getting the geometric index for this Geometry column.
if self._index:
if self.spatial_index:
return (self._geom_index(style, db_table),)
else:
return ()
def db_type(self):
"The OpenGIS name is returned for the MySQL database column type."
return self._geom
return self.geom_type
def get_placeholder(self, value):
"""
The placeholder here has to include MySQL's WKT constructor. Because
MySQL does not support spatial transformations, there is no need to
The placeholder here has to include MySQL's WKT constructor. Because
MySQL does not support spatial transformations, there is no need to
modify the placeholder based on the contents of the given value.
"""
return '%s(%%s)' % GEOM_FROM_TEXT

View File

@ -1,9 +1,10 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
from django.contrib.gis.db.backend.oracle.creation import create_test_spatial_db
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
from django.contrib.gis.db.backend.oracle.query import *
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
@ -29,4 +30,6 @@ SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
union=UNION,
Adaptor=OracleSpatialAdaptor,
Field=OracleSpatialField,
GeometryColumns=GeometryColumns,
SpatialRefSys=SpatialRefSys,
)

View File

@ -1,6 +1,5 @@
def create_spatial_db(test=True, verbosity=1, autoclobber=False):
def create_test_spatial_db(verbosity=1, autoclobber=False):
"A wrapper over the Oracle `create_test_db` routine."
if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py')
from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber)

View File

@ -32,26 +32,25 @@ class OracleSpatialField(Field):
Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA
table.
"""
# Checking the dimensions.
# TODO: Add support for 3D geometries.
if self._dim != 2:
if self.dim != 2:
raise Exception('3D geometries not yet supported on Oracle Spatial backend.')
# Constructing the SQL that will be used to insert information about
# the geometry column into the USER_GSDO_GEOM_METADATA table.
meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \
style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \
style.SQL_KEYWORD(' VALUES ') + '(\n ' + \
style.SQL_TABLE(gqn(db_table)) + ',\n ' + \
style.SQL_FIELD(gqn(self.column)) + ',\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \
' %s\n );' % self._srid
meta_sql = (style.SQL_KEYWORD('INSERT INTO ') +
style.SQL_TABLE('USER_SDO_GEOM_METADATA') +
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) +
style.SQL_KEYWORD(' VALUES ') + '(\n ' +
style.SQL_TABLE(gqn(db_table)) + ',\n ' +
style.SQL_FIELD(gqn(self.column)) + ',\n ' +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) +
' %s\n );' % self.srid)
return meta_sql
def _geom_index(self, style, db_table):
@ -60,14 +59,14 @@ class OracleSpatialField(Field):
# Getting the index name, Oracle doesn't allow object
# names > 30 characters.
idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30)
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
style.SQL_TABLE(qn(idx_name)) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + '(' + \
style.SQL_FIELD(qn(self.column)) + ') ' + \
style.SQL_KEYWORD('INDEXTYPE IS ') + \
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';'
sql = (style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn(idx_name)) +
style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + '(' +
style.SQL_FIELD(qn(self.column)) + ') ' +
style.SQL_KEYWORD('INDEXTYPE IS ') +
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';')
return sql
def post_create_sql(self, style, db_table):
@ -79,7 +78,7 @@ class OracleSpatialField(Field):
post_sql = self._add_geom(style, db_table)
# Getting the geometric index for this Geometry column.
if self._index:
if self.spatial_index:
return (post_sql, self._geom_index(style, db_table))
else:
return (post_sql,)
@ -87,7 +86,7 @@ class OracleSpatialField(Field):
def db_type(self):
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
return 'MDSYS.SDO_GEOMETRY'
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
@ -96,8 +95,8 @@ class OracleSpatialField(Field):
"""
if value is None:
return '%s'
elif value.srid != self._srid:
elif value.srid != self.srid:
# Adding Transform() to the SQL placeholder.
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self.srid)
else:
return 'SDO_GEOMETRY(%%s, %s)' % self._srid
return 'SDO_GEOMETRY(%%s, %s)' % self.srid

View File

@ -8,7 +8,6 @@
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
"""
from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
@ -22,7 +21,7 @@ class GeometryColumns(models.Model):
@classmethod
def table_name_col(cls):
"""
Returns the name of the metadata column used to store the
Returns the name of the metadata column used to store the
the feature table name.
"""
return 'table_name'
@ -30,7 +29,7 @@ class GeometryColumns(models.Model):
@classmethod
def geom_col_name(cls):
"""
Returns the name of the metadata column used to store the
Returns the name of the metadata column used to store the
the feature geometry column.
"""
return 'column_name'
@ -38,19 +37,19 @@ class GeometryColumns(models.Model):
def __unicode__(self):
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
class SpatialRefSys(models.Model):
"Maps to the Oracle MDSYS.CS_SRS table."
cs_name = models.CharField(max_length=68)
srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField()
auth_name = models.CharField(max_length=256)
wktext = models.CharField(max_length=2046)
#cs_bounds = models.GeometryField()
#cs_bounds = models.GeometryField() # TODO
class Meta:
# TODO: Figure out way to have this be MDSYS.CS_SRS without
# having django's quoting mess up the SQL.
abstract = True
db_table = 'CS_SRS'
app_label = '_mdsys' # Hack so that syncdb won't try to create "CS_SRS" table.
@property
def wkt(self):

View File

@ -1,14 +1,16 @@
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
from django.contrib.gis.db.backend.base import BaseSpatialBackend
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
from django.contrib.gis.db.backend.postgis.creation import create_test_spatial_db
from django.contrib.gis.db.backend.postgis.field import PostGISField
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
from django.contrib.gis.db.backend.postgis.query import *
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
area=AREA,
centroid=CENTROID,
collect=COLLECT,
difference=DIFFERENCE,
distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS,
@ -39,4 +41,6 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
Adaptor=PostGISAdaptor,
Field=PostGISField,
GeometryColumns=GeometryColumns,
SpatialRefSys=SpatialRefSys,
)

View File

@ -1,23 +1,10 @@
import os, re, sys
from subprocess import Popen, PIPE
from django.conf import settings
from django.core.management import call_command
from django.db import connection
from django.db.backends.creation import TEST_DATABASE_PREFIX
def getstatusoutput(cmd):
"""
Executes a shell command on the platform using subprocess.Popen and
return a tuple of the status and stdout output.
"""
# Set stdout and stderr to PIPE because we want to capture stdout and
# prevent stderr from displaying.
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
# We use p.communicate() instead of p.wait() to avoid deadlocks if the
# output buffers exceed POSIX buffer size.
stdout, stderr = p.communicate()
return p.returncode, stdout.strip()
from django.contrib.gis.db.backend.util import getstatusoutput
def create_lang(db_name, verbosity=1):
"Sets up the pl/pgsql language on the given database."
@ -110,20 +97,16 @@ def _create_with_shell(db_name, verbosity=1, autoclobber=False):
else:
raise Exception('Unknown error occurred in creating database: %s' % output)
def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False):
"Creates a spatial database based on the settings."
def create_test_spatial_db(verbosity=1, autoclobber=False, interactive=False):
"Creates a test spatial database based on the settings."
# Making sure we're using PostgreSQL and psycopg2
if settings.DATABASE_ENGINE != 'postgresql_psycopg2':
raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.')
# Getting the spatial database name
if test:
db_name = get_spatial_db(test=True)
_create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
else:
db_name = get_spatial_db()
_create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber)
db_name = get_spatial_db(test=True)
_create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
# If a template database is used, then don't need to do any of the following.
if not hasattr(settings, 'POSTGIS_TEMPLATE'):

View File

@ -19,35 +19,35 @@ class PostGISField(Field):
Takes the style object (provides syntax highlighting) and the
database table as parameters.
"""
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_TABLE('AddGeometryColumn') + '(' + \
style.SQL_TABLE(gqn(db_table)) + ', ' + \
style.SQL_FIELD(gqn(self.column)) + ', ' + \
style.SQL_FIELD(str(self._srid)) + ', ' + \
style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \
style.SQL_KEYWORD(str(self._dim)) + ');'
sql = (style.SQL_KEYWORD('SELECT ') +
style.SQL_TABLE('AddGeometryColumn') + '(' +
style.SQL_TABLE(gqn(db_table)) + ', ' +
style.SQL_FIELD(gqn(self.column)) + ', ' +
style.SQL_FIELD(str(self.srid)) + ', ' +
style.SQL_COLTYPE(gqn(self.geom_type)) + ', ' +
style.SQL_KEYWORD(str(self.dim)) + ');')
if not self.null:
# Add a NOT NULL constraint to the field
sql += '\n' + \
style.SQL_KEYWORD('ALTER TABLE ') + \
style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' ALTER ') + \
style.SQL_FIELD(qn(self.column)) + \
style.SQL_KEYWORD(' SET NOT NULL') + ';'
sql += ('\n' +
style.SQL_KEYWORD('ALTER TABLE ') +
style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' ALTER ') +
style.SQL_FIELD(qn(self.column)) +
style.SQL_KEYWORD(' SET NOT NULL') + ';')
return sql
def _geom_index(self, style, db_table,
index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):
"Creates a GiST index for this geometry field."
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + \
style.SQL_KEYWORD(' USING ') + \
style.SQL_COLTYPE(index_type) + ' ( ' + \
style.SQL_FIELD(qn(self.column)) + ' ' + \
style.SQL_KEYWORD(index_opts) + ' );'
sql = (style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) +
style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' USING ') +
style.SQL_COLTYPE(index_type) + ' ( ' +
style.SQL_FIELD(qn(self.column)) + ' ' +
style.SQL_KEYWORD(index_opts) + ' );')
return sql
def post_create_sql(self, style, db_table):
@ -62,17 +62,17 @@ class PostGISField(Field):
post_sql = self._add_geom(style, db_table)
# If the user wants to index this data, then get the indexing SQL as well.
if self._index:
if self.spatial_index:
return (post_sql, self._geom_index(style, db_table))
else:
return (post_sql,)
def _post_delete_sql(self, style, db_table):
"Drops the geometry column."
sql = style.SQL_KEYWORD('SELECT ') + \
style.SQL_KEYWORD('DropGeometryColumn') + '(' + \
style.SQL_TABLE(gqn(db_table)) + ', ' + \
style.SQL_FIELD(gqn(self.column)) + ');'
sql = (style.SQL_KEYWORD('SELECT ') +
style.SQL_KEYWORD('DropGeometryColumn') + '(' +
style.SQL_TABLE(gqn(db_table)) + ', ' +
style.SQL_FIELD(gqn(self.column)) + ');')
return sql
def db_type(self):
@ -88,8 +88,8 @@ class PostGISField(Field):
SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call.
"""
if value is None or value.srid == self._srid:
if value is None or value.srid == self.srid:
return '%s'
else:
# Adding Transform() to the SQL placeholder.
return '%s(%%s, %s)' % (TRANSFORM, self._srid)
return '%s(%%s, %s)' % (TRANSFORM, self.srid)

View File

@ -2,12 +2,6 @@
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):
"""
@ -28,7 +22,7 @@ class GeometryColumns(models.Model):
@classmethod
def table_name_col(cls):
"""
Returns the name of the metadata column used to store the
Returns the name of the metadata column used to store the
the feature table name.
"""
return 'f_table_name'
@ -36,7 +30,7 @@ class GeometryColumns(models.Model):
@classmethod
def geom_col_name(cls):
"""
Returns the name of the metadata column used to store the
Returns the name of the metadata column used to store the
the feature geometry column.
"""
return 'f_geometry_column'
@ -46,7 +40,7 @@ class GeometryColumns(models.Model):
(self.f_table_name, self.f_geometry_column,
self.coord_dimension, self.type, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
class SpatialRefSys(models.Model):
"""
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
documentaiton at Ch. 4.2.1.
@ -58,6 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
proj4text = models.CharField(max_length=2048)
class Meta:
abstract = True
db_table = 'spatial_ref_sys'
@property

View File

@ -42,6 +42,7 @@ if MAJOR_VERSION >= 1:
ASGML = get_func('AsGML')
ASSVG = get_func('AsSVG')
CENTROID = get_func('Centroid')
COLLECT = get_func('Collect')
DIFFERENCE = get_func('Difference')
DISTANCE = get_func('Distance')
DISTANCE_SPHERE = get_func('distance_sphere')

View File

@ -1,4 +1,21 @@
from types import UnicodeType
"""
A collection of utility routines and classes used by the spatial
backends.
"""
def getstatusoutput(cmd):
"""
Executes a shell command on the platform using subprocess.Popen and
return a tuple of the status and stdout output.
"""
from subprocess import Popen, PIPE
# Set stdout and stderr to PIPE because we want to capture stdout and
# prevent stderr from displaying.
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
# We use p.communicate() instead of p.wait() to avoid deadlocks if the
# output buffers exceed POSIX buffer size.
stdout, stderr = p.communicate()
return p.returncode, stdout.strip()
def gqn(val):
"""
@ -7,7 +24,7 @@ def gqn(val):
backend quotename function).
"""
if isinstance(val, basestring):
if isinstance(val, UnicodeType): val = val.encode('ascii')
if isinstance(val, unicode): val = val.encode('ascii')
return "'%s'" % val
else:
return str(val)

View File

@ -7,9 +7,6 @@ from django.contrib.gis.db.models.aggregates import *
# The GeoManager
from django.contrib.gis.db.models.manager import GeoManager
# The GeoQ object
from django.contrib.gis.db.models.query import GeoQ
# The geographic-enabled fields.
from django.contrib.gis.db.models.fields import \
GeometryField, PointField, LineStringField, PolygonField, \

View File

@ -5,7 +5,7 @@ from django.contrib.gis.db.models.sql import GeomField
class GeoAggregate(Aggregate):
def add_to_query(self, query, alias, col, source, is_summary):
if hasattr(source, '_geom'):
if hasattr(source, 'geom_type'):
# Doing additional setup on the Query object for spatial aggregates.
aggregate = getattr(query.aggregates_module, self.name)
@ -18,6 +18,9 @@ class GeoAggregate(Aggregate):
super(GeoAggregate, self).add_to_query(query, alias, col, source, is_summary)
class Collect(GeoAggregate):
name = 'Collect'
class Extent(GeoAggregate):
name = 'Extent'

View File

@ -8,12 +8,16 @@ from django.contrib.gis.measure import Distance
# reference system table w/o using the ORM.
from django.contrib.gis.models import get_srid_info
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
def deprecated_property(func):
from warnings import warn
warn('This attribute has been deprecated, pleas use "%s" instead.' % func.__name__[1:])
return property(func)
class GeometryField(SpatialBackend.Field):
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name.
_geom = 'GEOMETRY'
geom_type = 'GEOMETRY'
# Geodetic units.
geodetic_units = ('Decimal Degree', 'degree')
@ -37,15 +41,15 @@ class GeometryField(SpatialBackend.Field):
"""
# Setting the index flag with the value of the `spatial_index` keyword.
self._index = spatial_index
self.spatial_index = spatial_index
# Setting the SRID and getting the units. Unit information must be
# easily available in the field instance for distance queries.
self._srid = srid
self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
self.srid = srid
self.units, self.units_name, self._spheroid = get_srid_info(srid)
# Setting the dimension of the geometry field.
self._dim = dim
self.dim = dim
# Setting the verbose_name keyword argument with the positional
# first parameter, so this works like normal fields.
@ -53,6 +57,29 @@ class GeometryField(SpatialBackend.Field):
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
# The following properties are for formerly private variables that are now
# public for GeometryField. Because of their use by third-party applications,
# a deprecation warning is issued to notify them to use new attribute name.
def _deprecated_warning(self, old_name, new_name):
from warnings import warn
warn('The `%s` attribute name is deprecated, please update your code to use `%s` instead.' %
(old_name, new_name))
@property
def _geom(self):
self._deprecated_warning('_geom', 'geom_type')
return self.geom_type
@property
def _index(self):
self._deprecated_warning('_index', 'spatial_index')
return self.spatial_index
@property
def _srid(self):
self._deprecated_warning('_srid', 'srid')
return self.srid
### Routines specific to GeometryField ###
@property
def geodetic(self):
@ -60,7 +87,7 @@ class GeometryField(SpatialBackend.Field):
Returns true if this field's SRID corresponds with a coordinate
system that uses non-projected units (e.g., latitude/longitude).
"""
return self._unit_name in self.geodetic_units
return self.units_name in self.geodetic_units
def get_distance(self, dist_val, lookup_type):
"""
@ -80,7 +107,7 @@ class GeometryField(SpatialBackend.Field):
# Spherical distance calculation parameter should be in meters.
dist_param = dist.m
else:
dist_param = getattr(dist, Distance.unit_attname(self._unit_name))
dist_param = getattr(dist, Distance.unit_attname(self.units_name))
else:
# Assuming the distance is in the units of the field.
dist_param = dist
@ -127,8 +154,8 @@ class GeometryField(SpatialBackend.Field):
has no SRID, then that of the field will be returned.
"""
gsrid = geom.srid # SRID of given geometry.
if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1):
return self._srid
if gsrid is None or self.srid == -1 or (gsrid == -1 and self.srid != -1):
return self.srid
else:
return gsrid
@ -141,8 +168,9 @@ class GeometryField(SpatialBackend.Field):
def formfield(self, **kwargs):
defaults = {'form_class' : forms.GeometryField,
'geom_type' : self._geom,
'null' : self.null,
'geom_type' : self.geom_type,
'srid' : self.srid,
}
defaults.update(kwargs)
return super(GeometryField, self).formfield(**defaults)
@ -190,22 +218,22 @@ class GeometryField(SpatialBackend.Field):
# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
_geom = 'POINT'
geom_type = 'POINT'
class LineStringField(GeometryField):
_geom = 'LINESTRING'
geom_type = 'LINESTRING'
class PolygonField(GeometryField):
_geom = 'POLYGON'
geom_type = 'POLYGON'
class MultiPointField(GeometryField):
_geom = 'MULTIPOINT'
geom_type = 'MULTIPOINT'
class MultiLineStringField(GeometryField):
_geom = 'MULTILINESTRING'
geom_type = 'MULTILINESTRING'
class MultiPolygonField(GeometryField):
_geom = 'MULTIPOLYGON'
geom_type = 'MULTIPOLYGON'
class GeometryCollectionField(GeometryField):
_geom = 'GEOMETRYCOLLECTION'
geom_type = 'GEOMETRYCOLLECTION'

View File

@ -1,11 +0,0 @@
# 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,
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

@ -44,13 +44,13 @@ class GeometryProxy(object):
be used to set the geometry as well.
"""
# The OGC Geometry type of the field.
gtype = self._field._geom
gtype = self._field.geom_type
# 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
if value.srid is None: value.srid = self._field.srid
elif isinstance(value, (NoneType, StringType, UnicodeType)):
# Set with None, WKT, or HEX
pass

View File

@ -9,10 +9,6 @@ from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField
from django.contrib.gis.measure import Area, Distance
from django.contrib.gis.models import get_srid_info
# For backwards-compatibility; Q object should work just fine
# after queryset-refactor.
class GeoQ(Q): pass
class GeomSQL(object):
"Simple wrapper object for geometric SQL."
def __init__(self, geo_sql):
@ -44,10 +40,10 @@ class GeoQuerySet(QuerySet):
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
elif SpatialBackend.postgis:
elif SpatialBackend.postgis or SpatialBackend.spatialite:
if not geo_field.geodetic:
# Getting the area units of the geographic field.
s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name))
else:
# TODO: Do we want to support raw number areas for geodetic fields?
raise Exception('Area on geodetic coordinate systems not supported.')
@ -196,10 +192,18 @@ class GeoQuerySet(QuerySet):
Scales the geometry to a new size by multiplying the ordinates
with the given x,y,z scale factors.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
if SpatialBackend.spatialite:
if z != 0.0:
raise NotImplementedError('SpatiaLite does not support 3D scaling.')
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
'procedure_args' : {'x' : x, 'y' : y},
'select_field' : GeomField(),
}
else:
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('scale', s, **kwargs)
def svg(self, **kwargs):
@ -226,10 +230,18 @@ class GeoQuerySet(QuerySet):
Translates the geometry to a new location using the given numeric
parameters as offsets.
"""
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
if SpatialBackend.spatialite:
if z != 0.0:
raise NotImplementedError('SpatiaLite does not support 3D translation.')
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
'procedure_args' : {'x' : x, 'y' : y},
'select_field' : GeomField(),
}
else:
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(),
}
return self._spatial_attribute('translate', s, **kwargs)
def transform(self, srid=4326, **kwargs):
@ -415,7 +427,7 @@ class GeoQuerySet(QuerySet):
if geo_field.geodetic:
dist_att = 'm'
else:
dist_att = Distance.unit_attname(geo_field._unit_name)
dist_att = Distance.unit_attname(geo_field.units_name)
# Shortcut booleans for what distance function we're using.
distance = func == 'distance'
@ -430,7 +442,7 @@ class GeoQuerySet(QuerySet):
lookup_params = [geom or 'POINT (0 0)', 0]
# If the spheroid calculation is desired, either by the `spheroid`
# keyword or wehn calculating the length of geodetic field, make
# keyword or when calculating the length of geodetic field, make
# sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure.
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
@ -456,6 +468,9 @@ class GeoQuerySet(QuerySet):
else:
geodetic = geo_field.geodetic
if SpatialBackend.spatialite and geodetic:
raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.')
if distance:
if self.query.transformed_srid:
# Setting the `geom_args` flag to false because we want to handle
@ -467,12 +482,22 @@ class GeoQuerySet(QuerySet):
if geom.srid is None or geom.srid == self.query.transformed_srid:
# If the geom parameter srid is None, it is assumed the coordinates
# are in the transformed units. A placeholder is used for the
# geometry parameter.
procedure_fmt += ', %%s'
# geometry parameter. `GeomFromText` constructor is also needed
# to wrap geom placeholder for SpatiaLite.
if SpatialBackend.spatialite:
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.from_text, self.query.transformed_srid)
else:
procedure_fmt += ', %%s'
else:
# We need to transform the geom to the srid specified in `transform()`,
# so wrapping the geometry placeholder in transformation SQL.
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
# SpatiaLite also needs geometry placeholder wrapped in `GeomFromText`
# constructor.
if SpatialBackend.spatialite:
procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (SpatialBackend.transform, SpatialBackend.from_text,
geom.srid, self.query.transformed_srid)
else:
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
else:
# `transform()` was not used on this GeoQuerySet.
procedure_fmt = '%(geo_col)s,%(geom)s'
@ -483,9 +508,9 @@ class GeoQuerySet(QuerySet):
# procedures may only do queries from point columns to point geometries
# some error checking is required.
if not isinstance(geo_field, PointField):
raise TypeError('Spherical distance calculation only supported on PointFields.')
raise ValueError('Spherical distance calculation only supported on PointFields.')
if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
# The `function` procedure argument needs to be set differently for
# geodetic distance calculations.
if spheroid:

View File

@ -44,8 +44,17 @@ elif SpatialBackend.oracle:
return None
def convert_geom(clob, geo_field):
if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
else: return None
if clob:
return SpatialBackend.Geometry(clob.read(), geo_field.srid)
else:
return None
elif SpatialBackend.spatialite:
# SpatiaLite returns WKT.
def convert_geom(wkt, geo_field):
if wkt:
return SpatialBackend.Geometry(wkt, geo_field.srid)
else:
return None
class GeoAggregate(Aggregate):
# Overriding the SQL template with the geographic one.
@ -71,6 +80,10 @@ class GeoAggregate(Aggregate):
if not self.sql_function:
raise NotImplementedError('This aggregate functionality not implemented for your spatial backend.')
class Collect(GeoAggregate):
conversion_class = GeomField
sql_function = SpatialBackend.collect
class Extent(GeoAggregate):
is_extent = True
sql_function = SpatialBackend.extent

View File

@ -2,11 +2,15 @@
This module holds simple classes used by GeoQuery.convert_values
to convert geospatial values from the database.
"""
from django.contrib.gis.db.backend import SpatialBackend
class BaseField(object):
def get_internal_type(self):
"Overloaded method so OracleQuery.convert_values doesn't balk."
return None
if SpatialBackend.oracle: BaseField.empty_strings_allowed = False
class AreaField(BaseField):
"Wrapper for Area values."
def __init__(self, area_att):

View File

@ -208,13 +208,14 @@ class GeoQuery(sql.Query):
if SpatialBackend.oracle:
# Running through Oracle's first.
value = super(GeoQuery, self).convert_values(value, field or GeomField())
if isinstance(field, DistanceField):
# Using the field's distance attribute, can instantiate
# `Distance` with the right context.
value = Distance(**{field.distance_att : value})
elif isinstance(field, AreaField):
value = Area(**{field.area_att : value})
elif isinstance(field, GeomField):
elif isinstance(field, GeomField) and value:
value = SpatialBackend.Geometry(value)
return value
@ -260,7 +261,7 @@ class GeoQuery(sql.Query):
selection formats in order to retrieve geometries in OGC WKT. For all
other fields a simple '%s' format string is returned.
"""
if SpatialBackend.select and hasattr(fld, '_geom'):
if SpatialBackend.select and hasattr(fld, 'geom_type'):
# This allows operations to be done on fields in the SELECT,
# overriding their values -- used by the Oracle and MySQL
# spatial backends to get database values as WKT, and by the
@ -270,8 +271,10 @@ class GeoQuery(sql.Query):
# Because WKT doesn't contain spatial reference information,
# the SRID is prefixed to the returned WKT to ensure that the
# transformed geometries have an SRID different than that of the
# field -- this is only used by `transform` for Oracle backends.
if self.transformed_srid and SpatialBackend.oracle:
# field -- this is only used by `transform` for Oracle and
# SpatiaLite backends.
if self.transformed_srid and ( SpatialBackend.oracle or
SpatialBackend.spatialite ):
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
else:
sel_fmt = '%s'

View File

@ -15,7 +15,7 @@ class GeoAnnotation(object):
"""
def __init__(self, field, value, where):
self.geodetic = field.geodetic
self.geom_type = field._geom
self.geom_type = field.geom_type
self.value = value
self.where = tuple(where)
@ -37,7 +37,7 @@ class GeoWhereNode(WhereNode):
obj, lookup_type, value = data
alias, col, field = obj.alias, obj.col, obj.field
if not hasattr(field, "_geom"):
if not hasattr(field, "geom_type"):
# Not a geographic field, so call `WhereNode.add`.
return super(GeoWhereNode, self).add(data, connector)
else:
@ -50,7 +50,7 @@ class GeoWhereNode(WhereNode):
# Get the SRID of the geometry field that the expression was meant
# to operate on -- it's needed to determine whether transformation
# SQL is necessary.
srid = geo_fld._srid
srid = geo_fld.srid
# Getting the quoted representation of the geometry column that
# the expression is operating on.
@ -58,8 +58,8 @@ class GeoWhereNode(WhereNode):
# If it's in a different SRID, we'll need to wrap in
# transformation SQL.
if not srid is None and srid != field._srid and SpatialBackend.transform:
placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field._srid)
if not srid is None and srid != field.srid and SpatialBackend.transform:
placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field.srid)
else:
placeholder = '%s'

View File

@ -19,6 +19,7 @@ class GeometryField(forms.Field):
def __init__(self, **kwargs):
self.null = kwargs.pop('null')
self.geom_type = kwargs.pop('geom_type')
self.srid = kwargs.pop('srid')
super(GeometryField, self).__init__(**kwargs)
def clean(self, value):

View File

@ -15,24 +15,24 @@ class SpatialRefSysMixin(object):
The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code.
"""
# For pulling out the spheroid from the spatial reference string. This
# regular expression is used only if the user does not have GDAL installed.
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
# parameter.
# TODO: Flattening not used in all ellipsoids, could also be a minor axis,
# or 'b' parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
# For pulling out the units on platforms w/o GDAL installed.
# TODO: Figure out how to pull out angular units of projected coordinate system and
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
# distance queries.
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
def srs(self):
"""
Returns a GDAL SpatialReference object, if GDAL is installed.
"""
if HAS_GDAL:
# TODO: Is caching really necessary here? Is complexity worth it?
if hasattr(self, '_srs'):
# Returning a clone of the cached SpatialReference object.
return self._srs.clone()
@ -45,7 +45,13 @@ class SpatialRefSysMixin(object):
return self.srs
except Exception, msg:
pass
try:
self._srs = SpatialReference(self.proj4text)
return self.srs
except Exception, msg:
pass
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
else:
raise Exception('GDAL is not installed.')
@ -107,7 +113,7 @@ class SpatialRefSysMixin(object):
"Returns the linear units name."
if HAS_GDAL:
return self.srs.linear_name
elif self.geographic:
elif self.geographic:
return None
else:
m = self.units_regex.match(self.wkt)
@ -181,13 +187,13 @@ class SpatialRefSysMixin(object):
sphere_name = srs['spheroid']
else:
m = cls.spheroid_regex.match(wkt)
if m:
if m:
sphere_params = (float(m.group('major')), float(m.group('flattening')))
sphere_name = m.group('name')
else:
else:
return None
if not string:
if not string:
return sphere_name, sphere_params
else:
# `string` parameter used to place in format acceptable by PostGIS
@ -195,7 +201,7 @@ class SpatialRefSysMixin(object):
radius, flattening = sphere_params[0], sphere_params[2]
else:
radius, flattening = sphere_params
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
get_spheroid = classmethod(get_spheroid)
def __unicode__(self):
@ -208,76 +214,84 @@ class SpatialRefSysMixin(object):
except:
return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
_srid_info = True
if not PYTHON23 and settings.DATABASE_ENGINE == 'postgresql_psycopg2':
# Because the PostGIS version is checked when initializing the spatial
# backend a `ProgrammingError` will be raised if the PostGIS tables
# and functions are not installed. We catch here so it won't be raised when
# running the Django test suite. `ImportError` is also possible if no ctypes.
# Defining dummy default first; if spatial db, will overrride.
def get_srid_info(srid):
"""
Dummy routine for the backends that do not have the OGC required
spatial metadata tables (like MySQL).
"""
return None, None, None
# Django test suite on 2.3 platforms will choke on code inside this
# conditional.
if not PYTHON23:
try:
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
# try/except'ing the importation of SpatialBackend. Have to fail
# silently because this module may be inadvertently invoked by
# non-GeoDjango users (e.g., when the Django test suite executes
# the models.py of all contrib apps).
from django.contrib.gis.db.backend import SpatialBackend
if SpatialBackend.mysql: raise Exception
# Exposing the SpatialRefSys and GeometryColumns models.
class SpatialRefSys(SpatialBackend.SpatialRefSys, SpatialRefSysMixin):
pass
GeometryColumns = SpatialBackend.GeometryColumns
# Override `get_srid_info` with something real thing.
def get_srid_info(srid):
"""
Returns the units, unit name, and spheroid WKT associated with the
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
table. We use a database cursor to execute the query because this
function is used when it is not possible to use the ORM (for example,
during field initialization).
"""
# SRID=-1 is a common convention for indicating the geometry has no
# spatial reference information associated with it. Thus, we will
# return all None values without raising an exception.
if srid == -1: return None, None, None
# Getting the spatial reference WKT associated with the SRID from the
# `spatial_ref_sys` (or equivalent) spatial database table. This query
# cannot be executed using the ORM because this information is needed
# when the ORM cannot be used (e.g., during the initialization of
# `GeometryField`).
from django.db import connection
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
params = {'table' : qn(SpatialRefSys._meta.db_table),
'srid_col' : qn('srid'),
'srid' : srid,
}
if SpatialBackend.spatialite:
if not HAS_GDAL: raise Exception('GDAL is required to use the SpatiaLite backend.')
params['wkt_col'] = 'proj4text'
else:
params['wkt_col'] = qn(SpatialRefSys.wkt_col())
# Executing the SQL statement.
cur.execute(stmt % params)
# Fetching the WKT from the cursor; if the query failed raise an Exception.
fetched = cur.fetchone()
if not fetched:
raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' %
(SpatialRefSys._meta.db_table, srid))
if SpatialBackend.spatialite:
# Because the `spatial_ref_sys` table does _not_ contain a WKT column,
# we have to use GDAL to determine the units from the PROJ.4 string.
srs_wkt = SpatialReference(fetched[0]).wkt
else:
srs_wkt = fetched[0]
# Getting metadata associated with the spatial reference system identifier.
# Specifically, getting the unit information and spheroid information
# (both required for distance queries).
unit, unit_name = SpatialRefSys.get_units(srs_wkt)
spheroid = SpatialRefSys.get_spheroid(srs_wkt)
return unit, unit_name, spheroid
except:
_srid_info = False
elif not PYTHON23 and settings.DATABASE_ENGINE == 'oracle':
# Same thing as above, except the GEOS library is attempted to be loaded for
# `BaseSpatialBackend`, and an exception will be raised during the
# Django test suite if it doesn't exist.
try:
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
except:
_srid_info = False
else:
_srid_info = False
if _srid_info:
def get_srid_info(srid):
"""
Returns the units, unit name, and spheroid WKT associated with the
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
table. We use a database cursor to execute the query because this
function is used when it is not possible to use the ORM (for example,
during field initialization).
"""
# SRID=-1 is a common convention for indicating the geometry has no
# spatial reference information associated with it. Thus, we will
# return all None values without raising an exception.
if srid == -1: return None, None, None
# Getting the spatial reference WKT associated with the SRID from the
# `spatial_ref_sys` (or equivalent) spatial database table. This query
# cannot be executed using the ORM because this information is needed
# when the ORM cannot be used (e.g., during the initialization of
# `GeometryField`).
from django.db import connection
cur = connection.cursor()
qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'),
'srid' : srid,
}
cur.execute(stmt)
# Fetching the WKT from the cursor; if the query failed raise an Exception.
fetched = cur.fetchone()
if not fetched:
raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' %
(SpatialRefSys._meta.db_table, srid))
srs_wkt = fetched[0]
# Getting metadata associated with the spatial reference system identifier.
# Specifically, getting the unit information and spheroid information
# (both required for distance queries).
unit, unit_name = SpatialRefSys.get_units(srs_wkt)
spheroid = SpatialRefSys.get_spheroid(srs_wkt)
return unit, unit_name, spheroid
else:
def get_srid_info(srid):
"""
Dummy routine for the backends that do not have the OGC required
spatial metadata tables (like MySQL).
"""
return None, None, None
pass

View File

@ -9,28 +9,26 @@ def geo_suite():
some backends).
"""
from django.conf import settings
from django.contrib.gis.tests.utils import mysql, oracle, postgis
from django.contrib.gis import gdal, utils
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.utils import HAS_GEOIP
from django.contrib.gis.tests.utils import mysql
# The test suite.
s = unittest.TestSuite()
# Adding the GEOS tests. (__future__)
from django.contrib.gis.geos import tests as geos_tests
s.addTest(geos_tests.suite())
# Test apps that require use of a spatial database (e.g., creation of models)
# Tests that require use of a spatial database (e.g., creation of models)
test_apps = ['geoapp', 'relatedapp']
if oracle or postgis:
test_apps.append('distapp')
# Tests that do not require setting up and tearing down a spatial database
# and are modules in `django.contrib.gis.tests`.
# Tests that do not require setting up and tearing down a spatial database.
test_suite_names = [
'test_measure',
]
if gdal.HAS_GDAL:
# Tests applications that require a test spatial db.
if not mysql:
test_apps.append('distapp')
if HAS_GDAL:
# These tests require GDAL.
test_suite_names.append('test_spatialrefsys')
test_apps.append('layermap')
@ -39,14 +37,25 @@ def geo_suite():
from django.contrib.gis.gdal import tests as gdal_tests
s.addTest(gdal_tests.suite())
else:
print >>sys.stderr, "GDAL not available - no GDAL tests will be run."
print >>sys.stderr, "GDAL not available - no tests requiring GDAL will be run."
if utils.HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'):
if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'):
test_suite_names.append('test_geoip')
# Adding the rest of the suites from the modules specified
# in the `test_suite_names`.
for suite_name in test_suite_names:
tsuite = import_module('django.contrib.gis.tests.' + suite_name)
s.addTest(tsuite.suite())
# Adding the GEOS tests _last_. Doing this because if suite starts
# immediately with this test while after running syncdb, it will cause a
# segmentation fault. My initial guess is that SpatiaLite is still in
# critical areas of non thread-safe GEOS code when the test suite is run.
# TODO: Confirm my reasoning. Are there other consequences?
from django.contrib.gis.geos import tests as geos_tests
s.addTest(geos_tests.suite())
return s, test_apps
def run_gis_tests(test_labels, **kwargs):
@ -84,8 +93,8 @@ def run_gis_tests(test_labels, **kwargs):
# Creating the test suite, adding the test models to INSTALLED_APPS, and
# adding the model test suites to our suite package.
gis_suite, test_apps = geo_suite()
for test_app in test_apps:
module_name = 'django.contrib.gis.tests.%s' % test_app
for test_model in test_apps:
module_name = 'django.contrib.gis.tests.%s' % test_model
if mysql:
test_module = 'tests_mysql'
else:
@ -114,12 +123,12 @@ def run_gis_tests(test_labels, **kwargs):
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=None):
"""
This module allows users to run tests for GIS apps that require the creation
This module allows users to run tests for GIS apps that require the creation
of a spatial database. Currently, this is only required for PostgreSQL as
PostGIS needs extra overhead in test database creation.
In order to create a PostGIS database, the DATABASE_USER (or
TEST_DATABASE_USER, if defined) will require superuser priviliges.
In order to create a PostGIS database, the DATABASE_USER (or
TEST_DATABASE_USER, if defined) will require superuser priviliges.
To accomplish this outside the `postgres` user, you have a few options:
(A) Make your user a super user:
@ -133,11 +142,11 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
(B) Create your own PostgreSQL database as a local user:
1. Initialize database: `initdb -D /path/to/user/db`
2. If there's already a Postgres instance on the machine, it will need
to use a different TCP port than 5432. Edit postgresql.conf (in
/path/to/user/db) to change the database port (e.g. `port = 5433`).
to use a different TCP port than 5432. Edit postgresql.conf (in
/path/to/user/db) to change the database port (e.g. `port = 5433`).
3. Start this database `pg_ctl -D /path/to/user/db start`
(C) On Windows platforms the pgAdmin III utility may also be used as
(C) On Windows platforms the pgAdmin III utility may also be used as
a simple way to add superuser privileges to your database user.
The TEST_RUNNER needs to be set in your settings like so:
@ -145,10 +154,10 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
TEST_RUNNER='django.contrib.gis.tests.run_tests'
Note: This test runner assumes that the PostGIS SQL files ('lwpostgis.sql'
and 'spatial_ref_sys.sql') are installed in the directory specified by
and 'spatial_ref_sys.sql') are installed in the directory specified by
`pg_config --sharedir` (and defaults to /usr/local/share if that fails).
This behavior is overridden if POSTGIS_SQL_PATH is set in your settings.
Windows users should set POSTGIS_SQL_PATH manually because the output
of `pg_config` uses paths like 'C:/PROGRA~1/POSTGR~1/..'.
@ -160,18 +169,21 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
from django.test.simple import build_suite, build_test
from django.test.utils import setup_test_environment, teardown_test_environment
# The `create_spatial_db` routine abstracts away all the steps needed
# The `create_test_spatial_db` routine abstracts away all the steps needed
# to properly construct a spatial database for the backend.
from django.contrib.gis.db.backend import create_spatial_db
from django.contrib.gis.db.backend import create_test_spatial_db
# Setting up for testing.
setup_test_environment()
settings.DEBUG = False
old_name = settings.DATABASE_NAME
# Creating the test spatial database.
create_test_spatial_db(verbosity=verbosity)
# The suite may be passed in manually, e.g., when we run the GeoDjango test,
# we want to build it and pass it in due to some customizations. Otherwise,
# the normal test suite creation process from `django.test.simple.run_tests`
# we want to build it and pass it in due to some customizations. Otherwise,
# the normal test suite creation process from `django.test.simple.run_tests`
# is used to create the test suite.
if suite is None:
suite = unittest.TestSuite()
@ -185,13 +197,10 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
else:
for app in get_apps():
suite.addTest(build_suite(app))
for test in extra_tests:
suite.addTest(test)
# Creating the test spatial database.
create_spatial_db(test=True, verbosity=verbosity)
# Executing the tests (including the model tests), and destorying the
# test database after the tests have completed.
result = unittest.TextTestRunner(verbosity=verbosity).run(suite)

View File

@ -31,3 +31,6 @@ stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95
interstates = (('I-25', 'LINESTRING(-104.4780170766108 36.66698791870694, -104.4468522338495 36.79925409393386, -104.46212692626 36.9372149776075, -104.5126119783768 37.08163268820887, -104.5247764602161 37.29300499892048, -104.7084397427668 37.49150259925398, -104.8126599016282 37.69514285621863, -104.8452887035466 37.87613395659479, -104.7160169341003 38.05951763337799, -104.6165437927668 38.30432045855106, -104.6437227858174 38.53979986564737, -104.7596170387259 38.7322907594295, -104.8380078676822 38.89998460604341, -104.8501253693506 39.09980189213358, -104.8791648316464 39.24368776457503, -104.8635041274215 39.3785278162751, -104.8894471170052 39.5929228239605, -104.9721242843344 39.69528482419685, -105.0112104500356 39.7273080432394, -105.0010368577104 39.76677607811571, -104.981835619 39.81466504121967, -104.9858891550477 39.88806911250832, -104.9873548059578 39.98117234571016, -104.9766220487419 40.09796423450692, -104.9818565932953 40.36056530662884, -104.9912746373997 40.74904484447656)'),
)
stx_interstates = (('I-10', 'LINESTRING(924952.5 4220931.6,925065.3 4220931.6,929568.4 4221057.8)'),
)

View File

@ -37,6 +37,13 @@ class SouthTexasZipcode(models.Model):
class Interstate(models.Model):
"Geodetic model for U.S. Interstates."
name = models.CharField(max_length=10)
line = models.LineStringField()
path = models.LineStringField()
objects = models.GeoManager()
def __unicode__(self): return self.name
class SouthTexasInterstate(models.Model):
"Projected model for South Texas Interstates."
name = models.CharField(max_length=10)
path = models.LineStringField(srid=32140)
objects = models.GeoManager()
def __unicode__(self): return self.name

View File

@ -5,11 +5,11 @@ from django.db.models import Q
from django.contrib.gis.gdal import DataSource
from django.contrib.gis.geos import GEOSGeometry, Point, LineString
from django.contrib.gis.measure import D # alias for Distance
from django.contrib.gis.db.models import GeoQ
from django.contrib.gis.tests.utils import oracle, postgis, no_oracle
from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite
from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
from data import au_cities, interstates, stx_cities, stx_zips
from models import AustraliaCity, Interstate, SouthTexasInterstate, \
SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
from data import au_cities, interstates, stx_interstates, stx_cities, stx_zips
class DistanceTest(unittest.TestCase):
@ -32,9 +32,12 @@ class DistanceTest(unittest.TestCase):
# Loading up the cities.
def load_cities(city_model, data_tup):
for name, x, y in data_tup:
c = city_model(name=name, point=Point(x, y, srid=4326))
c.save()
city_model(name=name, point=Point(x, y, srid=4326)).save()
def load_interstates(imodel, data_tup):
for name, wkt in data_tup:
imodel(name=name, path=wkt).save()
load_cities(SouthTexasCity, stx_cities)
load_cities(SouthTexasCityFt, stx_cities)
load_cities(AustraliaCity, au_cities)
@ -42,7 +45,7 @@ class DistanceTest(unittest.TestCase):
self.assertEqual(9, SouthTexasCity.objects.count())
self.assertEqual(9, SouthTexasCityFt.objects.count())
self.assertEqual(11, AustraliaCity.objects.count())
# Loading up the South Texas Zip Codes.
for name, wkt in stx_zips:
poly = GEOSGeometry(wkt, srid=4269)
@ -52,10 +55,13 @@ class DistanceTest(unittest.TestCase):
self.assertEqual(4, CensusZipcode.objects.count())
# Loading up the Interstates.
for name, wkt in interstates:
Interstate(name=name, line=GEOSGeometry(wkt, srid=4326)).save()
self.assertEqual(1, Interstate.objects.count())
load_interstates(Interstate, interstates)
load_interstates(SouthTexasInterstate, stx_interstates)
self.assertEqual(1, Interstate.objects.count())
self.assertEqual(1, SouthTexasInterstate.objects.count())
@no_spatialite
def test02_dwithin(self):
"Testing the `dwithin` lookup type."
# Distances -- all should be equal (except for the
@ -63,7 +69,7 @@ class DistanceTest(unittest.TestCase):
# approximate).
tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)]
au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)]
# Expected cities for Australia and Texas.
tx_cities = ['Downtown Houston', 'Southside Place']
au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong']
@ -86,27 +92,29 @@ class DistanceTest(unittest.TestCase):
if isinstance(dist, tuple):
if oracle: dist = dist[1]
else: dist = dist[0]
# Creating the query set.
qs = AustraliaCity.objects.order_by('name')
if type_error:
# A TypeError should be raised on PostGIS when trying to pass
# Distance objects into a DWithin query using a geodetic field.
# Distance objects into a DWithin query using a geodetic field.
self.assertRaises(TypeError, AustraliaCity.objects.filter, point__dwithin=(self.au_pnt, dist))
else:
self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist))))
def test03a_distance_method(self):
"Testing the `distance` GeoQuerySet method on projected coordinate systems."
# The point for La Grange, TX
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
# Reference distances in feet and in meters. Got these values from
# Reference distances in feet and in meters. Got these values from
# using the provided raw SQL statements.
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity;
m_distances = [147075.069813, 139630.198056, 140888.552826,
138809.684197, 158309.246259, 212183.594374,
70870.188967, 165337.758878, 139196.085105]
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft;
# Oracle 11 thinks this is not a projected coordinate system, so it's s
# not tested.
ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
455411.438904354, 519386.252102563, 696139.009211594,
232513.278304279, 542445.630586414, 456679.155883207]
@ -115,8 +123,12 @@ class DistanceTest(unittest.TestCase):
# with different projected coordinate systems.
dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
dist4 = SouthTexasCityFt.objects.distance(lagrange)
if spatialite or oracle:
dist_qs = [dist1, dist2]
else:
dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
dist4 = SouthTexasCityFt.objects.distance(lagrange)
dist_qs = [dist1, dist2, dist3, dist4]
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle.
@ -124,11 +136,12 @@ class DistanceTest(unittest.TestCase):
else: tol = 5
# Ensuring expected distances are returned for each distance queryset.
for qs in [dist1, dist2, dist3, dist4]:
for qs in dist_qs:
for i, c in enumerate(qs):
self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
@no_spatialite
def test03b_distance_method(self):
"Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
if oracle: tol = 2
@ -139,8 +152,8 @@ class DistanceTest(unittest.TestCase):
if not oracle:
# PostGIS is limited to disance queries only to/from point geometries,
# ensuring a TypeError is raised if something else is put in.
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
self.assertRaises(ValueError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
self.assertRaises(ValueError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
# Got the reference distances using the raw SQL statements:
# SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
@ -163,8 +176,8 @@ class DistanceTest(unittest.TestCase):
"Testing the `distance` GeoQuerySet method used with `transform` on a geographic field."
# Normally you can't compute distances from a geometry field
# that is not a PointField (on PostGIS).
self.assertRaises(TypeError, CensusZipcode.objects.distance, self.stx_pnt)
self.assertRaises(ValueError, CensusZipcode.objects.distance, self.stx_pnt)
# We'll be using a Polygon (created by buffering the centroid
# of 77005 to 100m) -- which aren't allowed in geographic distance
# queries normally, however our field has been transformed to
@ -182,9 +195,11 @@ class DistanceTest(unittest.TestCase):
# however.
buf1 = z.poly.centroid.buffer(100)
buf2 = buf1.transform(4269, clone=True)
ref_zips = ['77002', '77025', '77401']
for buf in [buf1, buf2]:
qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf)
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
self.assertEqual(ref_zips, self.get_names(qs))
for i, z in enumerate(qs):
self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
@ -194,8 +209,16 @@ class DistanceTest(unittest.TestCase):
# (thus, Houston and Southside place will be excluded as tested in
# the `test02_dwithin` above).
qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
for qs in qs1, qs2:
# Can't determine the units on SpatiaLite from PROJ.4 string, and
# Oracle 11 incorrectly thinks it is not projected.
if spatialite or oracle:
dist_qs = (qs1,)
else:
qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
dist_qs = (qs1, qs2)
for qs in dist_qs:
cities = self.get_names(qs)
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
@ -206,7 +229,8 @@ class DistanceTest(unittest.TestCase):
# If we add a little more distance 77002 should be included.
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300)))
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
@no_spatialite
def test05_geodetic_distance_lookups(self):
"Testing distance lookups on geodetic coordinate systems."
if not oracle:
@ -216,7 +240,7 @@ class DistanceTest(unittest.TestCase):
self.assertRaises(TypeError,
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
# Too many params (4 in this case) should raise a ValueError.
self.assertRaises(ValueError,
self.assertRaises(ValueError,
AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4'))
# Not enough params should raise a ValueError.
@ -235,14 +259,13 @@ class DistanceTest(unittest.TestCase):
d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles.
# Normal geodetic distance lookup (uses `distance_sphere` on PostGIS.
gq1 = GeoQ(point__distance_lte=(wollongong.point, d1))
gq2 = GeoQ(point__distance_gte=(wollongong.point, d2))
gq1 = Q(point__distance_lte=(wollongong.point, d1))
gq2 = Q(point__distance_gte=(wollongong.point, d2))
qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
# Geodetic distance lookup but telling GeoDjango to use `distance_spheroid`
# instead (we should get the same results b/c accuracy variance won't matter
# in this test case). Using `Q` instead of `GeoQ` to be different (post-qsrf
# it doesn't matter).
# in this test case).
if postgis:
gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
@ -270,12 +293,23 @@ class DistanceTest(unittest.TestCase):
"Testing the `length` GeoQuerySet method."
# Reference query (should use `length_spheroid`).
# SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]');
len_m = 473504.769553813
qs = Interstate.objects.length()
if oracle: tol = 2
else: tol = 5
self.assertAlmostEqual(len_m, qs[0].length.m, tol)
len_m1 = 473504.769553813
len_m2 = 4617.668
if spatialite:
# Does not support geodetic coordinate systems.
self.assertRaises(ValueError, Interstate.objects.length)
else:
qs = Interstate.objects.length()
if oracle: tol = 2
else: tol = 5
self.assertAlmostEqual(len_m1, qs[0].length.m, tol)
# Now doing length on a projected coordinate system.
i10 = SouthTexasInterstate.objects.length().get(name='I-10')
self.assertAlmostEqual(len_m2, i10.length.m, 2)
@no_spatialite
def test08_perimeter(self):
"Testing the `perimeter` GeoQuerySet method."
# Reference query:

View File

@ -1,5 +1,5 @@
from django.contrib.gis.db import models
from django.contrib.gis.tests.utils import mysql
from django.contrib.gis.tests.utils import mysql, spatialite
# MySQL spatial indices can't handle NULL geometries.
null_flag = not mysql
@ -12,7 +12,7 @@ class Country(models.Model):
class City(models.Model):
name = models.CharField(max_length=30)
point = models.PointField()
point = models.PointField()
objects = models.GeoManager()
def __unicode__(self): return self.name
@ -27,12 +27,13 @@ class State(models.Model):
objects = models.GeoManager()
def __unicode__(self): return self.name
class Feature(models.Model):
name = models.CharField(max_length=20)
geom = models.GeometryField()
objects = models.GeoManager()
def __unicode__(self): return self.name
if not spatialite:
class Feature(models.Model):
name = models.CharField(max_length=20)
geom = models.GeometryField()
objects = models.GeoManager()
def __unicode__(self): return self.name
class MinusOneSRID(models.Model):
geom = models.PointField(srid=-1) # Minus one SRID.
objects = models.GeoManager()
class MinusOneSRID(models.Model):
geom = models.PointField(srid=-1) # Minus one SRID.
objects = models.GeoManager()

View File

@ -1,10 +1,13 @@
import os, unittest
from models import Country, City, PennsylvaniaCity, State, Feature, MinusOneSRID
from django.contrib.gis import gdal
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.geos import *
from django.contrib.gis.measure import Distance
from django.contrib.gis.tests.utils import no_oracle, no_postgis
from django.contrib.gis.tests.utils import no_oracle, no_postgis, no_spatialite
from models import Country, City, PennsylvaniaCity, State
if not SpatialBackend.spatialite:
from models import Feature, MinusOneSRID
# TODO: Some tests depend on the success/failure of previous tests, these should
# be decoupled. This flag is an artifact of this problem, and makes debugging easier;
@ -37,9 +40,11 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(2, Country.objects.count())
self.assertEqual(8, City.objects.count())
# Oracle cannot handle NULL geometry values w/certain queries.
if SpatialBackend.oracle: n_state = 2
else: n_state = 3
# Only PostGIS can handle NULL geometries
if SpatialBackend.postgis or SpatialBackend.spatialite:
n_state = 3
else:
n_state = 2
self.assertEqual(n_state, State.objects.count())
def test02_proxy(self):
@ -112,6 +117,7 @@ class GeoModelTest(unittest.TestCase):
ns.delete()
@no_oracle # Oracle does not support KML.
@no_spatialite # SpatiaLite does not support KML.
def test03a_kml(self):
"Testing KML output from the database using GeoManager.kml()."
if DISABLE: return
@ -137,6 +143,7 @@ class GeoModelTest(unittest.TestCase):
for ptown in [ptown1, ptown2]:
self.assertEqual(ref_kml, ptown.kml)
@no_spatialite # SpatiaLite does not support GML.
def test03b_gml(self):
"Testing GML output from the database using GeoManager.gml()."
if DISABLE: return
@ -150,7 +157,7 @@ class GeoModelTest(unittest.TestCase):
if SpatialBackend.oracle:
# No precision parameter for Oracle :-/
import re
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925199\d+,38.25500\d+ </gml:coordinates></gml:Point>')
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ </gml:coordinates></gml:Point>')
for ptown in [ptown1, ptown2]:
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
else:
@ -180,7 +187,7 @@ class GeoModelTest(unittest.TestCase):
self.assertAlmostEqual(ptown.x, p.point.x, prec)
self.assertAlmostEqual(ptown.y, p.point.y, prec)
@no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
@no_spatialite # SpatiaLite does not have an Extent function
def test05_extent(self):
"Testing the `extent` GeoQuerySet method."
if DISABLE: return
@ -193,9 +200,10 @@ class GeoModelTest(unittest.TestCase):
extent = qs.extent()
for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 8)
self.assertAlmostEqual(exp, val, 4)
@no_oracle
@no_spatialite # SpatiaLite does not have a MakeLine function
def test06_make_line(self):
"Testing the `make_line` GeoQuerySet method."
if DISABLE: return
@ -214,10 +222,13 @@ class GeoModelTest(unittest.TestCase):
qs1 = City.objects.filter(point__disjoint=ptown.point)
self.assertEqual(7, qs1.count())
if not SpatialBackend.postgis:
if not (SpatialBackend.postgis or SpatialBackend.spatialite):
# TODO: Do NULL columns bork queries on PostGIS? The following
# error is encountered:
# psycopg2.ProgrammingError: invalid memory alloc request size 4294957297
#
# Similarly, on SpatiaLite Puerto Rico is also returned (could be a
# manifestation of
qs2 = State.objects.filter(poly__disjoint=ptown.point)
self.assertEqual(1, qs2.count())
self.assertEqual('Kansas', qs2[0].name)
@ -248,10 +259,13 @@ class GeoModelTest(unittest.TestCase):
# Houston and Wellington.
tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry
nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX
ks = State.objects.get(poly__contains=lawrence.point)
self.assertEqual('Texas', tx.name)
self.assertEqual('New Zealand', nz.name)
self.assertEqual('Kansas', ks.name)
# Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry).
if not SpatialBackend.spatialite:
ks = State.objects.get(poly__contains=lawrence.point)
self.assertEqual('Kansas', ks.name)
# Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
# are not contained in Texas or New Zealand.
@ -304,13 +318,16 @@ class GeoModelTest(unittest.TestCase):
# If the GeometryField SRID is -1, then we shouldn't perform any
# transformation if the SRID of the input geometry is different.
m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
m1.save()
self.assertEqual(-1, m1.geom.srid)
# SpatiaLite does not support missing SRID values.
if not SpatialBackend.spatialite:
m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
m1.save()
self.assertEqual(-1, m1.geom.srid)
# Oracle does not support NULL geometries in its spatial index for
# some routines (e.g., SDO_GEOM.RELATE).
@no_oracle
@no_spatialite
def test12_null_geometries(self):
"Testing NULL geometry support, and the `isnull` lookup type."
if DISABLE: return
@ -334,6 +351,7 @@ class GeoModelTest(unittest.TestCase):
State(name='Northern Mariana Islands', poly=None).save()
@no_oracle # No specific `left` or `right` operators in Oracle.
@no_spatialite # No `left` or `right` operators in SpatiaLite.
def test13_left_right(self):
"Testing the 'left' and 'right' lookup types."
if DISABLE: return
@ -398,7 +416,7 @@ class GeoModelTest(unittest.TestCase):
self.assertRaises(e, qs.count)
# Relate works differently for the different backends.
if SpatialBackend.postgis:
if SpatialBackend.postgis or SpatialBackend.spatialite:
contains_mask = 'T*T***FF*'
within_mask = 'T*F**F***'
intersects_mask = 'T********'
@ -449,9 +467,12 @@ class GeoModelTest(unittest.TestCase):
union = union1
self.assertEqual(True, union.equals_exact(u1, tol))
self.assertEqual(True, union.equals_exact(u2, tol))
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.unionagg(field_name='point'))
# SpatiaLite will segfault trying to union a NULL geometry.
if not SpatialBackend.spatialite:
qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.unionagg(field_name='point'))
@no_spatialite # SpatiaLite does not support abstract geometry columns
def test18_geometryfield(self):
"Testing GeometryField."
if DISABLE: return
@ -479,8 +500,12 @@ class GeoModelTest(unittest.TestCase):
"Testing the `centroid` GeoQuerySet method."
if DISABLE: return
qs = State.objects.exclude(poly__isnull=True).centroid()
if SpatialBackend.oracle: tol = 0.1
else: tol = 0.000000001
if SpatialBackend.oracle:
tol = 0.1
elif SpatialBackend.spatialite:
tol = 0.000001
else:
tol = 0.000000001
for s in qs:
self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
@ -493,14 +518,19 @@ class GeoModelTest(unittest.TestCase):
ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326),
}
elif SpatialBackend.postgis:
elif SpatialBackend.postgis or SpatialBackend.spatialite:
# Using GEOSGeometry to compute the reference point on surface values
# -- since PostGIS also uses GEOS these should be the same.
ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface,
'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface
}
for cntry in Country.objects.point_on_surface():
self.assertEqual(ref[cntry.name], cntry.point_on_surface)
if SpatialBackend.spatialite:
# XXX This seems to be a WKT-translation-related precision issue?
tol = 0.00001
else: tol = 0.000000001
self.assertEqual(True, ref[cntry.name].equals_exact(cntry.point_on_surface, tol))
@no_oracle
def test21_scale(self):
@ -512,8 +542,9 @@ class GeoModelTest(unittest.TestCase):
for p1, p2 in zip(c.mpoly, c.scaled):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] * xfac, c2[0])
self.assertEqual(c1[1] * yfac, c2[1])
# XXX The low precision is for SpatiaLite
self.assertAlmostEqual(c1[0] * xfac, c2[0], 5)
self.assertAlmostEqual(c1[1] * yfac, c2[1], 5)
@no_oracle
def test22_translate(self):
@ -525,8 +556,9 @@ class GeoModelTest(unittest.TestCase):
for p1, p2 in zip(c.mpoly, c.translated):
for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] + xfac, c2[0])
self.assertEqual(c1[1] + yfac, c2[1])
# XXX The low precision is for SpatiaLite
self.assertAlmostEqual(c1[0] + xfac, c2[0], 5)
self.assertAlmostEqual(c1[1] + yfac, c2[1], 5)
def test23_numgeom(self):
"Testing the `num_geom` GeoQuerySet method."
@ -539,27 +571,46 @@ class GeoModelTest(unittest.TestCase):
if SpatialBackend.postgis: self.assertEqual(None, c.num_geom)
else: self.assertEqual(1, c.num_geom)
@no_spatialite # SpatiaLite can only count vertices in LineStrings
def test24_numpoints(self):
"Testing the `num_points` GeoQuerySet method."
if DISABLE: return
for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points)
if SpatialBackend.postgis:
for c in Country.objects.num_points():
self.assertEqual(c.mpoly.num_points, c.num_points)
if not SpatialBackend.oracle:
# Oracle cannot count vertices in Point geometries.
for c in City.objects.num_points(): self.assertEqual(1, c.num_points)
@no_oracle
def test25_geoset(self):
"Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
if DISABLE: return
geom = Point(5, 23)
for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom):
self.assertEqual(c.mpoly.difference(geom), c.difference)
self.assertEqual(c.mpoly.intersection(geom), c.intersection)
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
self.assertEqual(c.mpoly.union(geom), c.union)
tol = 1
qs = Country.objects.all().difference(geom).sym_difference(geom).union(geom)
# XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also,
# XXX it doesn't like the null intersection.
if SpatialBackend.spatialite:
qs = qs.exclude(name='Texas')
else:
qs = qs.intersection(geom)
for c in qs:
if SpatialBackend.oracle:
# Should be able to execute the queries; however, they won't be the same
# as GEOS (because Oracle doesn't use GEOS internally like PostGIS or
# SpatiaLite).
pass
else:
self.assertEqual(c.mpoly.difference(geom), c.difference)
if not SpatialBackend.spatialite:
self.assertEqual(c.mpoly.intersection(geom), c.intersection)
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
self.assertEqual(c.mpoly.union(geom), c.union)
def test26_inherited_geofields(self):
"Test GeoQuerySet methods on inherited Geometry fields."
if DISABLE: return
# Creating a Pennsylvanian city.
mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)')

View File

@ -7,9 +7,9 @@ from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, I
from django.contrib.gis.gdal import DataSource
shp_path = os.path.dirname(__file__)
city_shp = os.path.join(shp_path, 'cities/cities.shp')
co_shp = os.path.join(shp_path, 'counties/counties.shp')
inter_shp = os.path.join(shp_path, 'interstates/interstates.shp')
city_shp = os.path.join(shp_path, '../data/cities/cities.shp')
co_shp = os.path.join(shp_path, '../data/counties/counties.shp')
inter_shp = os.path.join(shp_path, '../data/interstates/interstates.shp')
# Dictionaries to hold what's expected in the county shapefile.
NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo']
@ -53,7 +53,6 @@ class LayerMapTest(unittest.TestCase):
def test02_simple_layermap(self):
"Test LayerMapping import of a simple point shapefile."
# Setting up for the LayerMapping.
lm = LayerMapping(City, city_shp, city_mapping)
lm.save()
@ -78,7 +77,6 @@ class LayerMapTest(unittest.TestCase):
def test03_layermap_strict(self):
"Testing the `strict` keyword, and import of a LineString shapefile."
# When the `strict` keyword is set an error encountered will force
# the importation to stop.
try:
@ -118,7 +116,6 @@ class LayerMapTest(unittest.TestCase):
def county_helper(self, county_feat=True):
"Helper function for ensuring the integrity of the mapped County models."
for name, n, st in zip(NAMES, NUMS, STATES):
# Should only be one record b/c of `unique` keyword.
c = County.objects.get(name=name)
@ -198,7 +195,6 @@ class LayerMapTest(unittest.TestCase):
def test05_test_fid_range_step(self):
"Tests the `fid_range` keyword and the `step` keyword of .save()."
# Function for clearing out all the counties before testing.
def clear_counties(): County.objects.all().delete()

View File

@ -2,7 +2,7 @@ import os, unittest
from django.contrib.gis.geos import *
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models import F, Extent, Union
from django.contrib.gis.tests.utils import no_mysql, no_oracle
from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite
from django.conf import settings
from models import City, Location, DirectoryEntry, Parcel
@ -12,7 +12,7 @@ cities = (('Aurora', 'TX', -97.516111, 33.058333),
)
class RelatedGeoModelTest(unittest.TestCase):
def test01_setup(self):
"Setting up for related model tests."
for name, state, lon, lat in cities:
@ -32,7 +32,7 @@ class RelatedGeoModelTest(unittest.TestCase):
self.assertEqual(nm, c.name)
self.assertEqual(st, c.state)
self.assertEqual(Point(lon, lat), c.location.point)
@no_mysql
@no_oracle # Pagination problem is implicated in this test as well.
def test03_transform_related(self):
@ -43,7 +43,7 @@ class RelatedGeoModelTest(unittest.TestCase):
tol = 3
else:
tol = 0
def check_pnt(ref, pnt):
self.assertAlmostEqual(ref.x, pnt.x, tol)
self.assertAlmostEqual(ref.y, pnt.y, tol)
@ -59,14 +59,14 @@ class RelatedGeoModelTest(unittest.TestCase):
# Doing this implicitly sets `select_related` select the location.
# TODO: Fix why this breaks on Oracle.
qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
@no_mysql
def test04_related_aggregate(self):
"Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models."
@no_spatialite
def test04a_related_extent_aggregate(self):
"Testing the `extent` GeoQuerySet aggregates on related geographic models."
# This combines the Extent and Union aggregates into one query
aggs = City.objects.aggregate(Extent('location__point'), Union('location__point'))
aggs = City.objects.aggregate(Extent('location__point'))
# One for all locations, one that excludes Roswell.
all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
@ -81,14 +81,20 @@ class RelatedGeoModelTest(unittest.TestCase):
for ref, e in [(all_extent, e1), (txpa_extent, e2), (all_extent, e3)]:
for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val, tol)
@no_mysql
def test04b_related_union_aggregate(self):
"Testing the `unionagg` GeoQuerySet aggregates on related geographic models."
# This combines the Extent and Union aggregates into one query
aggs = City.objects.aggregate(Union('location__point'))
# These are the points that are components of the aggregate geographic
# union that is returned.
p1 = Point(-104.528056, 33.387222)
p2 = Point(-97.516111, 33.058333)
p3 = Point(-79.460734, 40.18476)
# Creating the reference union geometry depending on the spatial backend,
# as Oracle will have a different internal ordering of the component
# as Oracle will have a different internal ordering of the component
# geometries than PostGIS. The second union aggregate is for a union
# query that includes limiting information in the WHERE clause (in other
# words a `.filter()` precedes the call to `.unionagg()`).
@ -98,7 +104,7 @@ class RelatedGeoModelTest(unittest.TestCase):
else:
ref_u1 = MultiPoint(p1, p2, p3, srid=4326)
ref_u2 = MultiPoint(p2, p3, srid=4326)
u1 = City.objects.unionagg(field_name='location__point')
u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point')
u3 = aggs['location__point__union']
@ -106,7 +112,7 @@ class RelatedGeoModelTest(unittest.TestCase):
self.assertEqual(ref_u1, u1)
self.assertEqual(ref_u2, u2)
self.assertEqual(ref_u1, u3)
def test05_select_related_fk_to_subclass(self):
"Testing that calling select_related on a query over a model with an FK to a model subclass works"
# Regression test for #9752.
@ -140,14 +146,14 @@ class RelatedGeoModelTest(unittest.TestCase):
qs = Parcel.objects.filter(center1__within=F('border1'))
self.assertEqual(1, len(qs))
self.assertEqual('P2', qs[0].name)
if not SpatialBackend.mysql:
# This time center2 is in a different coordinate system and needs
# to be wrapped in transformation SQL.
qs = Parcel.objects.filter(center2__within=F('border1'))
self.assertEqual(1, len(qs))
self.assertEqual('P2', qs[0].name)
# Should return the first Parcel, which has the center point equal
# to the point in the City ForeignKey.
qs = Parcel.objects.filter(center1=F('city__location__point'))

View File

@ -15,8 +15,10 @@ def no_backend(test_func, backend):
def no_oracle(func): return no_backend(func, 'oracle')
def no_postgis(func): return no_backend(func, 'postgresql_psycopg2')
def no_mysql(func): return no_backend(func, 'mysql')
def no_spatialite(func): return no_backend(func, 'sqlite3')
# Shortcut booleans to omit only portions of tests.
oracle = settings.DATABASE_ENGINE == 'oracle'
postgis = settings.DATABASE_ENGINE == 'postgresql_psycopg2'
mysql = settings.DATABASE_ENGINE == 'mysql'
spatialite = settings.DATABASE_ENGINE == 'sqlite3'