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. # Retrieving the necessary settings from the backend.
if settings.DATABASE_ENGINE == 'postgresql_psycopg2': 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': 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': 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: else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)

View File

@ -24,6 +24,3 @@ class BaseSpatialBackend(object):
except KeyError: except KeyError:
return False 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.base import BaseSpatialBackend
from django.contrib.gis.db.backend.adaptor import WKTAdaptor 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.field import MySQLGeoField
from django.contrib.gis.db.backend.mysql.query import * 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): def create_test_spatial_db(verbosity=1, autoclobber=False):
if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py') "A wrapper over the MySQL `create_test_db` method."
from django.db import connection from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber) connection.creation.create_test_db(verbosity, autoclobber)

View File

@ -22,11 +22,11 @@ class MySQLGeoField(Field):
# Getting the index name. # Getting the index name.
idx_name = '%s_%s_id' % (db_table, self.column) idx_name = '%s_%s_id' % (db_table, self.column)
sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \ sql = (style.SQL_KEYWORD('CREATE SPATIAL INDEX ') +
style.SQL_TABLE(qn(idx_name)) + \ style.SQL_TABLE(qn(idx_name)) +
style.SQL_KEYWORD(' ON ') + \ style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + '(' + \ style.SQL_TABLE(qn(db_table)) + '(' +
style.SQL_FIELD(qn(self.column)) + ');' style.SQL_FIELD(qn(self.column)) + ');')
return sql return sql
def post_create_sql(self, style, db_table): def post_create_sql(self, style, db_table):
@ -35,14 +35,14 @@ class MySQLGeoField(Field):
created. created.
""" """
# Getting the geometric index for this Geometry column. # Getting the geometric index for this Geometry column.
if self._index: if self.spatial_index:
return (self._geom_index(style, db_table),) return (self._geom_index(style, db_table),)
else: else:
return () return ()
def db_type(self): def db_type(self):
"The OpenGIS name is returned for the MySQL database column type." "The OpenGIS name is returned for the MySQL database column type."
return self._geom return self.geom_type
def get_placeholder(self, value): def get_placeholder(self, value):
""" """

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.base import BaseSpatialBackend
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor 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.field import OracleSpatialField
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
from django.contrib.gis.db.backend.oracle.query import * from django.contrib.gis.db.backend.oracle.query import *
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True, SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
@ -29,4 +30,6 @@ SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
union=UNION, union=UNION,
Adaptor=OracleSpatialAdaptor, Adaptor=OracleSpatialAdaptor,
Field=OracleSpatialField, 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." "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 from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber) 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 Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA
table. table.
""" """
# Checking the dimensions. # Checking the dimensions.
# TODO: Add support for 3D geometries. # 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.') raise Exception('3D geometries not yet supported on Oracle Spatial backend.')
# Constructing the SQL that will be used to insert information about # Constructing the SQL that will be used to insert information about
# the geometry column into the USER_GSDO_GEOM_METADATA table. # the geometry column into the USER_GSDO_GEOM_METADATA table.
meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \ meta_sql = (style.SQL_KEYWORD('INSERT INTO ') +
style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \ style.SQL_TABLE('USER_SDO_GEOM_METADATA') +
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \ ' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) +
style.SQL_KEYWORD(' VALUES ') + '(\n ' + \ style.SQL_KEYWORD(' VALUES ') + '(\n ' +
style.SQL_TABLE(gqn(db_table)) + ',\n ' + \ style.SQL_TABLE(gqn(db_table)) + ',\n ' +
style.SQL_FIELD(gqn(self.column)) + ',\n ' + \ style.SQL_FIELD(gqn(self.column)) + ',\n ' +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \ style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \ ("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) +
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \ ("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) +
' %s\n );' % self._srid ' %s\n );' % self.srid)
return meta_sql return meta_sql
def _geom_index(self, style, db_table): def _geom_index(self, style, db_table):
@ -61,13 +60,13 @@ class OracleSpatialField(Field):
# names > 30 characters. # names > 30 characters.
idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30) idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30)
sql = style.SQL_KEYWORD('CREATE INDEX ') + \ sql = (style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn(idx_name)) + \ style.SQL_TABLE(qn(idx_name)) +
style.SQL_KEYWORD(' ON ') + \ style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + '(' + \ style.SQL_TABLE(qn(db_table)) + '(' +
style.SQL_FIELD(qn(self.column)) + ') ' + \ style.SQL_FIELD(qn(self.column)) + ') ' +
style.SQL_KEYWORD('INDEXTYPE IS ') + \ style.SQL_KEYWORD('INDEXTYPE IS ') +
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';' style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';')
return sql return sql
def post_create_sql(self, style, db_table): def post_create_sql(self, style, db_table):
@ -79,7 +78,7 @@ class OracleSpatialField(Field):
post_sql = self._add_geom(style, db_table) post_sql = self._add_geom(style, db_table)
# Getting the geometric index for this Geometry column. # Getting the geometric index for this Geometry column.
if self._index: if self.spatial_index:
return (post_sql, self._geom_index(style, db_table)) return (post_sql, self._geom_index(style, db_table))
else: else:
return (post_sql,) return (post_sql,)
@ -96,8 +95,8 @@ class OracleSpatialField(Field):
""" """
if value is None: if value is None:
return '%s' return '%s'
elif value.srid != self._srid: elif value.srid != self.srid:
# Adding Transform() to the SQL placeholder. # 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: 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. model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
""" """
from django.db import models from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
class GeometryColumns(models.Model): class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table." "Maps to the Oracle USER_SDO_GEOM_METADATA table."
@ -38,19 +37,19 @@ class GeometryColumns(models.Model):
def __unicode__(self): def __unicode__(self):
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid) 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." "Maps to the Oracle MDSYS.CS_SRS table."
cs_name = models.CharField(max_length=68) cs_name = models.CharField(max_length=68)
srid = models.IntegerField(primary_key=True) srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField() auth_srid = models.IntegerField()
auth_name = models.CharField(max_length=256) auth_name = models.CharField(max_length=256)
wktext = models.CharField(max_length=2046) wktext = models.CharField(max_length=2046)
#cs_bounds = models.GeometryField() #cs_bounds = models.GeometryField() # TODO
class Meta: class Meta:
# TODO: Figure out way to have this be MDSYS.CS_SRS without abstract = True
# having django's quoting mess up the SQL.
db_table = 'CS_SRS' db_table = 'CS_SRS'
app_label = '_mdsys' # Hack so that syncdb won't try to create "CS_SRS" table.
@property @property
def wkt(self): 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.base import BaseSpatialBackend
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor 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.field import PostGISField
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
from django.contrib.gis.db.backend.postgis.query import * from django.contrib.gis.db.backend.postgis.query import *
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
area=AREA, area=AREA,
centroid=CENTROID, centroid=CENTROID,
collect=COLLECT,
difference=DIFFERENCE, difference=DIFFERENCE,
distance=DISTANCE, distance=DISTANCE,
distance_functions=DISTANCE_FUNCTIONS, distance_functions=DISTANCE_FUNCTIONS,
@ -39,4 +41,6 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2), version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
Adaptor=PostGISAdaptor, Adaptor=PostGISAdaptor,
Field=PostGISField, Field=PostGISField,
GeometryColumns=GeometryColumns,
SpatialRefSys=SpatialRefSys,
) )

View File

@ -1,23 +1,10 @@
import os, re, sys import os, re, sys
from subprocess import Popen, PIPE
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.db import connection from django.db import connection
from django.db.backends.creation import TEST_DATABASE_PREFIX from django.db.backends.creation import TEST_DATABASE_PREFIX
from django.contrib.gis.db.backend.util import getstatusoutput
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()
def create_lang(db_name, verbosity=1): def create_lang(db_name, verbosity=1):
"Sets up the pl/pgsql language on the given database." "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: else:
raise Exception('Unknown error occurred in creating database: %s' % output) raise Exception('Unknown error occurred in creating database: %s' % output)
def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): def create_test_spatial_db(verbosity=1, autoclobber=False, interactive=False):
"Creates a spatial database based on the settings." "Creates a test spatial database based on the settings."
# Making sure we're using PostgreSQL and psycopg2 # Making sure we're using PostgreSQL and psycopg2
if settings.DATABASE_ENGINE != 'postgresql_psycopg2': if settings.DATABASE_ENGINE != 'postgresql_psycopg2':
raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.') raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.')
# Getting the spatial database name # Getting the spatial database name
if test:
db_name = get_spatial_db(test=True) db_name = get_spatial_db(test=True)
_create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
else:
db_name = get_spatial_db()
_create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber)
# If a template database is used, then don't need to do any of the following. # If a template database is used, then don't need to do any of the following.
if not hasattr(settings, 'POSTGIS_TEMPLATE'): if not hasattr(settings, 'POSTGIS_TEMPLATE'):

View File

@ -19,35 +19,35 @@ class PostGISField(Field):
Takes the style object (provides syntax highlighting) and the Takes the style object (provides syntax highlighting) and the
database table as parameters. database table as parameters.
""" """
sql = style.SQL_KEYWORD('SELECT ') + \ sql = (style.SQL_KEYWORD('SELECT ') +
style.SQL_TABLE('AddGeometryColumn') + '(' + \ style.SQL_TABLE('AddGeometryColumn') + '(' +
style.SQL_TABLE(gqn(db_table)) + ', ' + \ style.SQL_TABLE(gqn(db_table)) + ', ' +
style.SQL_FIELD(gqn(self.column)) + ', ' + \ style.SQL_FIELD(gqn(self.column)) + ', ' +
style.SQL_FIELD(str(self._srid)) + ', ' + \ style.SQL_FIELD(str(self.srid)) + ', ' +
style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \ style.SQL_COLTYPE(gqn(self.geom_type)) + ', ' +
style.SQL_KEYWORD(str(self._dim)) + ');' style.SQL_KEYWORD(str(self.dim)) + ');')
if not self.null: if not self.null:
# Add a NOT NULL constraint to the field # Add a NOT NULL constraint to the field
sql += '\n' + \ sql += ('\n' +
style.SQL_KEYWORD('ALTER TABLE ') + \ style.SQL_KEYWORD('ALTER TABLE ') +
style.SQL_TABLE(qn(db_table)) + \ style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' ALTER ') + \ style.SQL_KEYWORD(' ALTER ') +
style.SQL_FIELD(qn(self.column)) + \ style.SQL_FIELD(qn(self.column)) +
style.SQL_KEYWORD(' SET NOT NULL') + ';' style.SQL_KEYWORD(' SET NOT NULL') + ';')
return sql return sql
def _geom_index(self, style, db_table, def _geom_index(self, style, db_table,
index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):
"Creates a GiST index for this geometry field." "Creates a GiST index for this geometry field."
sql = style.SQL_KEYWORD('CREATE INDEX ') + \ sql = (style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \ style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) +
style.SQL_KEYWORD(' ON ') + \ style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + \ style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' USING ') + \ style.SQL_KEYWORD(' USING ') +
style.SQL_COLTYPE(index_type) + ' ( ' + \ style.SQL_COLTYPE(index_type) + ' ( ' +
style.SQL_FIELD(qn(self.column)) + ' ' + \ style.SQL_FIELD(qn(self.column)) + ' ' +
style.SQL_KEYWORD(index_opts) + ' );' style.SQL_KEYWORD(index_opts) + ' );')
return sql return sql
def post_create_sql(self, style, db_table): def post_create_sql(self, style, db_table):
@ -62,17 +62,17 @@ class PostGISField(Field):
post_sql = self._add_geom(style, db_table) post_sql = self._add_geom(style, db_table)
# If the user wants to index this data, then get the indexing SQL as well. # 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)) return (post_sql, self._geom_index(style, db_table))
else: else:
return (post_sql,) return (post_sql,)
def _post_delete_sql(self, style, db_table): def _post_delete_sql(self, style, db_table):
"Drops the geometry column." "Drops the geometry column."
sql = style.SQL_KEYWORD('SELECT ') + \ sql = (style.SQL_KEYWORD('SELECT ') +
style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ style.SQL_KEYWORD('DropGeometryColumn') + '(' +
style.SQL_TABLE(gqn(db_table)) + ', ' + \ style.SQL_TABLE(gqn(db_table)) + ', ' +
style.SQL_FIELD(gqn(self.column)) + ');' style.SQL_FIELD(gqn(self.column)) + ');')
return sql return sql
def db_type(self): def db_type(self):
@ -88,8 +88,8 @@ class PostGISField(Field):
SRID of the field. Specifically, this routine will substitute in the SRID of the field. Specifically, this routine will substitute in the
ST_Transform() function call. ST_Transform() function call.
""" """
if value is None or value.srid == self._srid: if value is None or value.srid == self.srid:
return '%s' return '%s'
else: else:
# Adding Transform() to the SQL placeholder. # 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. The GeometryColumns and SpatialRefSys models for the PostGIS backend.
""" """
from django.db import models 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): class GeometryColumns(models.Model):
""" """
@ -46,7 +40,7 @@ class GeometryColumns(models.Model):
(self.f_table_name, self.f_geometry_column, (self.f_table_name, self.f_geometry_column,
self.coord_dimension, self.type, self.srid) 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 The 'spatial_ref_sys' table from PostGIS. See the PostGIS
documentaiton at Ch. 4.2.1. documentaiton at Ch. 4.2.1.
@ -58,6 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin):
proj4text = models.CharField(max_length=2048) proj4text = models.CharField(max_length=2048)
class Meta: class Meta:
abstract = True
db_table = 'spatial_ref_sys' db_table = 'spatial_ref_sys'
@property @property

View File

@ -42,6 +42,7 @@ if MAJOR_VERSION >= 1:
ASGML = get_func('AsGML') ASGML = get_func('AsGML')
ASSVG = get_func('AsSVG') ASSVG = get_func('AsSVG')
CENTROID = get_func('Centroid') CENTROID = get_func('Centroid')
COLLECT = get_func('Collect')
DIFFERENCE = get_func('Difference') DIFFERENCE = get_func('Difference')
DISTANCE = get_func('Distance') DISTANCE = get_func('Distance')
DISTANCE_SPHERE = get_func('distance_sphere') 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): def gqn(val):
""" """
@ -7,7 +24,7 @@ def gqn(val):
backend quotename function). backend quotename function).
""" """
if isinstance(val, basestring): if isinstance(val, basestring):
if isinstance(val, UnicodeType): val = val.encode('ascii') if isinstance(val, unicode): val = val.encode('ascii')
return "'%s'" % val return "'%s'" % val
else: else:
return str(val) return str(val)

View File

@ -7,9 +7,6 @@ from django.contrib.gis.db.models.aggregates import *
# The GeoManager # The GeoManager
from django.contrib.gis.db.models.manager import 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. # The geographic-enabled fields.
from django.contrib.gis.db.models.fields import \ from django.contrib.gis.db.models.fields import \
GeometryField, PointField, LineStringField, PolygonField, \ GeometryField, PointField, LineStringField, PolygonField, \

View File

@ -5,7 +5,7 @@ from django.contrib.gis.db.models.sql import GeomField
class GeoAggregate(Aggregate): class GeoAggregate(Aggregate):
def add_to_query(self, query, alias, col, source, is_summary): 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. # Doing additional setup on the Query object for spatial aggregates.
aggregate = getattr(query.aggregates_module, self.name) 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) super(GeoAggregate, self).add_to_query(query, alias, col, source, is_summary)
class Collect(GeoAggregate):
name = 'Collect'
class Extent(GeoAggregate): class Extent(GeoAggregate):
name = 'Extent' name = 'Extent'

View File

@ -8,12 +8,16 @@ from django.contrib.gis.measure import Distance
# reference system table w/o using the ORM. # reference system table w/o using the ORM.
from django.contrib.gis.models import get_srid_info 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): class GeometryField(SpatialBackend.Field):
"The base GIS field -- maps to the OpenGIS Specification Geometry type." "The base GIS field -- maps to the OpenGIS Specification Geometry type."
# The OpenGIS Geometry name. # The OpenGIS Geometry name.
_geom = 'GEOMETRY' geom_type = 'GEOMETRY'
# Geodetic units. # Geodetic units.
geodetic_units = ('Decimal Degree', 'degree') 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. # 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 # Setting the SRID and getting the units. Unit information must be
# easily available in the field instance for distance queries. # easily available in the field instance for distance queries.
self._srid = srid self.srid = srid
self._unit, self._unit_name, self._spheroid = get_srid_info(srid) self.units, self.units_name, self._spheroid = get_srid_info(srid)
# Setting the dimension of the geometry field. # Setting the dimension of the geometry field.
self._dim = dim self.dim = dim
# Setting the verbose_name keyword argument with the positional # Setting the verbose_name keyword argument with the positional
# first parameter, so this works like normal fields. # 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 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 ### ### Routines specific to GeometryField ###
@property @property
def geodetic(self): def geodetic(self):
@ -60,7 +87,7 @@ class GeometryField(SpatialBackend.Field):
Returns true if this field's SRID corresponds with a coordinate Returns true if this field's SRID corresponds with a coordinate
system that uses non-projected units (e.g., latitude/longitude). 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): def get_distance(self, dist_val, lookup_type):
""" """
@ -80,7 +107,7 @@ class GeometryField(SpatialBackend.Field):
# Spherical distance calculation parameter should be in meters. # Spherical distance calculation parameter should be in meters.
dist_param = dist.m dist_param = dist.m
else: else:
dist_param = getattr(dist, Distance.unit_attname(self._unit_name)) dist_param = getattr(dist, Distance.unit_attname(self.units_name))
else: else:
# Assuming the distance is in the units of the field. # Assuming the distance is in the units of the field.
dist_param = dist dist_param = dist
@ -127,8 +154,8 @@ class GeometryField(SpatialBackend.Field):
has no SRID, then that of the field will be returned. has no SRID, then that of the field will be returned.
""" """
gsrid = geom.srid # SRID of given geometry. gsrid = geom.srid # SRID of given geometry.
if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1): if gsrid is None or self.srid == -1 or (gsrid == -1 and self.srid != -1):
return self._srid return self.srid
else: else:
return gsrid return gsrid
@ -141,8 +168,9 @@ class GeometryField(SpatialBackend.Field):
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class' : forms.GeometryField, defaults = {'form_class' : forms.GeometryField,
'geom_type' : self._geom,
'null' : self.null, 'null' : self.null,
'geom_type' : self.geom_type,
'srid' : self.srid,
} }
defaults.update(kwargs) defaults.update(kwargs)
return super(GeometryField, self).formfield(**defaults) return super(GeometryField, self).formfield(**defaults)
@ -190,22 +218,22 @@ class GeometryField(SpatialBackend.Field):
# The OpenGIS Geometry Type Fields # The OpenGIS Geometry Type Fields
class PointField(GeometryField): class PointField(GeometryField):
_geom = 'POINT' geom_type = 'POINT'
class LineStringField(GeometryField): class LineStringField(GeometryField):
_geom = 'LINESTRING' geom_type = 'LINESTRING'
class PolygonField(GeometryField): class PolygonField(GeometryField):
_geom = 'POLYGON' geom_type = 'POLYGON'
class MultiPointField(GeometryField): class MultiPointField(GeometryField):
_geom = 'MULTIPOINT' geom_type = 'MULTIPOINT'
class MultiLineStringField(GeometryField): class MultiLineStringField(GeometryField):
_geom = 'MULTILINESTRING' geom_type = 'MULTILINESTRING'
class MultiPolygonField(GeometryField): class MultiPolygonField(GeometryField):
_geom = 'MULTIPOLYGON' geom_type = 'MULTIPOLYGON'
class GeometryCollectionField(GeometryField): 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. be used to set the geometry as well.
""" """
# The OGC Geometry type of the field. # 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 # The geometry type must match that of the field -- unless the
# general GeometryField is used. # general GeometryField is used.
if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
# Assigning the SRID to the 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)): elif isinstance(value, (NoneType, StringType, UnicodeType)):
# Set with None, WKT, or HEX # Set with None, WKT, or HEX
pass 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.measure import Area, Distance
from django.contrib.gis.models import get_srid_info 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): class GeomSQL(object):
"Simple wrapper object for geometric SQL." "Simple wrapper object for geometric SQL."
def __init__(self, geo_sql): def __init__(self, geo_sql):
@ -44,10 +40,10 @@ class GeoQuerySet(QuerySet):
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance s['procedure_args']['tolerance'] = tolerance
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. 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: if not geo_field.geodetic:
# Getting the area units of the geographic field. # 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: else:
# TODO: Do we want to support raw number areas for geodetic fields? # TODO: Do we want to support raw number areas for geodetic fields?
raise Exception('Area on geodetic coordinate systems not supported.') raise Exception('Area on geodetic coordinate systems not supported.')
@ -196,6 +192,14 @@ class GeoQuerySet(QuerySet):
Scales the geometry to a new size by multiplying the ordinates Scales the geometry to a new size by multiplying the ordinates
with the given x,y,z scale factors. with the given x,y,z scale factors.
""" """
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', s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, 'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(), 'select_field' : GeomField(),
@ -226,6 +230,14 @@ class GeoQuerySet(QuerySet):
Translates the geometry to a new location using the given numeric Translates the geometry to a new location using the given numeric
parameters as offsets. parameters as offsets.
""" """
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', s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, 'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
'select_field' : GeomField(), 'select_field' : GeomField(),
@ -415,7 +427,7 @@ class GeoQuerySet(QuerySet):
if geo_field.geodetic: if geo_field.geodetic:
dist_att = 'm' dist_att = 'm'
else: 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. # Shortcut booleans for what distance function we're using.
distance = func == 'distance' distance = func == 'distance'
@ -430,7 +442,7 @@ class GeoQuerySet(QuerySet):
lookup_params = [geom or 'POINT (0 0)', 0] lookup_params = [geom or 'POINT (0 0)', 0]
# If the spheroid calculation is desired, either by the `spheroid` # 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 # sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure. # get the correct spatial stored procedure.
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
@ -456,6 +468,9 @@ class GeoQuerySet(QuerySet):
else: else:
geodetic = geo_field.geodetic 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 distance:
if self.query.transformed_srid: if self.query.transformed_srid:
# Setting the `geom_args` flag to false because we want to handle # Setting the `geom_args` flag to false because we want to handle
@ -467,11 +482,21 @@ class GeoQuerySet(QuerySet):
if geom.srid is None or geom.srid == self.query.transformed_srid: if geom.srid is None or geom.srid == self.query.transformed_srid:
# If the geom parameter srid is None, it is assumed the coordinates # If the geom parameter srid is None, it is assumed the coordinates
# are in the transformed units. A placeholder is used for the # are in the transformed units. A placeholder is used for the
# geometry parameter. # 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' procedure_fmt += ', %%s'
else: else:
# We need to transform the geom to the srid specified in `transform()`, # We need to transform the geom to the srid specified in `transform()`,
# so wrapping the geometry placeholder in transformation SQL. # so wrapping the geometry placeholder in transformation SQL.
# 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) procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
else: else:
# `transform()` was not used on this GeoQuerySet. # `transform()` was not used on this GeoQuerySet.
@ -483,9 +508,9 @@ class GeoQuerySet(QuerySet):
# procedures may only do queries from point columns to point geometries # procedures may only do queries from point columns to point geometries
# some error checking is required. # some error checking is required.
if not isinstance(geo_field, PointField): 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': 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 # The `function` procedure argument needs to be set differently for
# geodetic distance calculations. # geodetic distance calculations.
if spheroid: if spheroid:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,11 +15,10 @@ class SpatialRefSysMixin(object):
The SpatialRefSysMixin is a class used by the database-dependent The SpatialRefSysMixin is a class used by the database-dependent
SpatialRefSys objects to reduce redundnant code. SpatialRefSys objects to reduce redundnant code.
""" """
# For pulling out the spheroid from the spatial reference string. This # For pulling out the spheroid from the spatial reference string. This
# regular expression is used only if the user does not have GDAL installed. # 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' # TODO: Flattening not used in all ellipsoids, could also be a minor axis,
# parameter. # or 'b' parameter.
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),') 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. # For pulling out the units on platforms w/o GDAL installed.
@ -33,6 +32,7 @@ class SpatialRefSysMixin(object):
Returns a GDAL SpatialReference object, if GDAL is installed. Returns a GDAL SpatialReference object, if GDAL is installed.
""" """
if HAS_GDAL: if HAS_GDAL:
# TODO: Is caching really necessary here? Is complexity worth it?
if hasattr(self, '_srs'): if hasattr(self, '_srs'):
# Returning a clone of the cached SpatialReference object. # Returning a clone of the cached SpatialReference object.
return self._srs.clone() return self._srs.clone()
@ -46,6 +46,12 @@ class SpatialRefSysMixin(object):
except Exception, msg: except Exception, msg:
pass 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)) raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
else: else:
raise Exception('GDAL is not installed.') raise Exception('GDAL is not installed.')
@ -208,29 +214,31 @@ class SpatialRefSysMixin(object):
except: except:
return unicode(self.wkt) return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models # Defining dummy default first; if spatial db, will overrride.
_srid_info = True def get_srid_info(srid):
if not PYTHON23 and settings.DATABASE_ENGINE == 'postgresql_psycopg2': """
# Because the PostGIS version is checked when initializing the spatial Dummy routine for the backends that do not have the OGC required
# backend a `ProgrammingError` will be raised if the PostGIS tables spatial metadata tables (like MySQL).
# 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. return None, None, None
try:
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
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: # Django test suite on 2.3 platforms will choke on code inside this
# conditional.
if not PYTHON23:
try:
# 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): def get_srid_info(srid):
""" """
Returns the units, unit name, and spheroid WKT associated with the Returns the units, unit name, and spheroid WKT associated with the
@ -253,18 +261,30 @@ if _srid_info:
cur = connection.cursor() cur = connection.cursor()
qn = connection.ops.quote_name qn = connection.ops.quote_name
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)' 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), params = {'table' : qn(SpatialRefSys._meta.db_table),
'wkt_col' : qn(SpatialRefSys.wkt_col()),
'srid_col' : qn('srid'), 'srid_col' : qn('srid'),
'srid' : srid, 'srid' : srid,
} }
cur.execute(stmt) 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. # Fetching the WKT from the cursor; if the query failed raise an Exception.
fetched = cur.fetchone() fetched = cur.fetchone()
if not fetched: if not fetched:
raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' % raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' %
(SpatialRefSys._meta.db_table, srid)) (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] srs_wkt = fetched[0]
# Getting metadata associated with the spatial reference system identifier. # Getting metadata associated with the spatial reference system identifier.
@ -273,11 +293,5 @@ if _srid_info:
unit, unit_name = SpatialRefSys.get_units(srs_wkt) unit, unit_name = SpatialRefSys.get_units(srs_wkt)
spheroid = SpatialRefSys.get_spheroid(srs_wkt) spheroid = SpatialRefSys.get_spheroid(srs_wkt)
return unit, unit_name, spheroid return unit, unit_name, spheroid
else: except:
def get_srid_info(srid): pass
"""
Dummy routine for the backends that do not have the OGC required
spatial metadata tables (like MySQL).
"""
return None, None, None

View File

@ -9,28 +9,26 @@ def geo_suite():
some backends). some backends).
""" """
from django.conf import settings from django.conf import settings
from django.contrib.gis.tests.utils import mysql, oracle, postgis from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis import gdal, utils from django.contrib.gis.utils import HAS_GEOIP
from django.contrib.gis.tests.utils import mysql
# The test suite. # The test suite.
s = unittest.TestSuite() s = unittest.TestSuite()
# Adding the GEOS tests. (__future__) # Tests that require use of a spatial database (e.g., creation of models)
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)
test_apps = ['geoapp', 'relatedapp'] 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 # Tests that do not require setting up and tearing down a spatial database.
# and are modules in `django.contrib.gis.tests`.
test_suite_names = [ test_suite_names = [
'test_measure', '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. # These tests require GDAL.
test_suite_names.append('test_spatialrefsys') test_suite_names.append('test_spatialrefsys')
test_apps.append('layermap') test_apps.append('layermap')
@ -39,14 +37,25 @@ def geo_suite():
from django.contrib.gis.gdal import tests as gdal_tests from django.contrib.gis.gdal import tests as gdal_tests
s.addTest(gdal_tests.suite()) s.addTest(gdal_tests.suite())
else: 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') 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: for suite_name in test_suite_names:
tsuite = import_module('django.contrib.gis.tests.' + suite_name) tsuite = import_module('django.contrib.gis.tests.' + suite_name)
s.addTest(tsuite.suite()) 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 return s, test_apps
def run_gis_tests(test_labels, **kwargs): 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 # Creating the test suite, adding the test models to INSTALLED_APPS, and
# adding the model test suites to our suite package. # adding the model test suites to our suite package.
gis_suite, test_apps = geo_suite() gis_suite, test_apps = geo_suite()
for test_app in test_apps: for test_model in test_apps:
module_name = 'django.contrib.gis.tests.%s' % test_app module_name = 'django.contrib.gis.tests.%s' % test_model
if mysql: if mysql:
test_module = 'tests_mysql' test_module = 'tests_mysql'
else: else:
@ -160,15 +169,18 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
from django.test.simple import build_suite, build_test from django.test.simple import build_suite, build_test
from django.test.utils import setup_test_environment, teardown_test_environment 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. # 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. # Setting up for testing.
setup_test_environment() setup_test_environment()
settings.DEBUG = False settings.DEBUG = False
old_name = settings.DATABASE_NAME 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, # 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, # 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` # the normal test suite creation process from `django.test.simple.run_tests`
@ -189,9 +201,6 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=
for test in extra_tests: for test in extra_tests:
suite.addTest(test) 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 # Executing the tests (including the model tests), and destorying the
# test database after the tests have completed. # test database after the tests have completed.
result = unittest.TextTestRunner(verbosity=verbosity).run(suite) 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)'), 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): class Interstate(models.Model):
"Geodetic model for U.S. Interstates." "Geodetic model for U.S. Interstates."
name = models.CharField(max_length=10) 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() objects = models.GeoManager()
def __unicode__(self): return self.name 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.gdal import DataSource
from django.contrib.gis.geos import GEOSGeometry, Point, LineString from django.contrib.gis.geos import GEOSGeometry, Point, LineString
from django.contrib.gis.measure import D # alias for Distance 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, spatialite, no_oracle, no_spatialite
from django.contrib.gis.tests.utils import oracle, postgis, no_oracle
from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode from models import AustraliaCity, Interstate, SouthTexasInterstate, \
from data import au_cities, interstates, stx_cities, stx_zips SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
from data import au_cities, interstates, stx_interstates, stx_cities, stx_zips
class DistanceTest(unittest.TestCase): class DistanceTest(unittest.TestCase):
@ -32,8 +32,11 @@ class DistanceTest(unittest.TestCase):
# Loading up the cities. # Loading up the cities.
def load_cities(city_model, data_tup): def load_cities(city_model, data_tup):
for name, x, y in data_tup: for name, x, y in data_tup:
c = city_model(name=name, point=Point(x, y, srid=4326)) city_model(name=name, point=Point(x, y, srid=4326)).save()
c.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(SouthTexasCity, stx_cities)
load_cities(SouthTexasCityFt, stx_cities) load_cities(SouthTexasCityFt, stx_cities)
@ -52,10 +55,13 @@ class DistanceTest(unittest.TestCase):
self.assertEqual(4, CensusZipcode.objects.count()) self.assertEqual(4, CensusZipcode.objects.count())
# Loading up the Interstates. # Loading up the Interstates.
for name, wkt in interstates: load_interstates(Interstate, interstates)
Interstate(name=name, line=GEOSGeometry(wkt, srid=4326)).save() load_interstates(SouthTexasInterstate, stx_interstates)
self.assertEqual(1, Interstate.objects.count())
self.assertEqual(1, Interstate.objects.count())
self.assertEqual(1, SouthTexasInterstate.objects.count())
@no_spatialite
def test02_dwithin(self): def test02_dwithin(self):
"Testing the `dwithin` lookup type." "Testing the `dwithin` lookup type."
# Distances -- all should be equal (except for the # Distances -- all should be equal (except for the
@ -107,6 +113,8 @@ class DistanceTest(unittest.TestCase):
138809.684197, 158309.246259, 212183.594374, 138809.684197, 158309.246259, 212183.594374,
70870.188967, 165337.758878, 139196.085105] 70870.188967, 165337.758878, 139196.085105]
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft; # 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, ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
455411.438904354, 519386.252102563, 696139.009211594, 455411.438904354, 519386.252102563, 696139.009211594,
232513.278304279, 542445.630586414, 456679.155883207] 232513.278304279, 542445.630586414, 456679.155883207]
@ -115,8 +123,12 @@ class DistanceTest(unittest.TestCase):
# with different projected coordinate systems. # with different projected coordinate systems.
dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point') dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
if spatialite or oracle:
dist_qs = [dist1, dist2]
else:
dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter. dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
dist4 = SouthTexasCityFt.objects.distance(lagrange) dist4 = SouthTexasCityFt.objects.distance(lagrange)
dist_qs = [dist1, dist2, dist3, dist4]
# Original query done on PostGIS, have to adjust AlmostEqual tolerance # Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle. # for Oracle.
@ -124,11 +136,12 @@ class DistanceTest(unittest.TestCase):
else: tol = 5 else: tol = 5
# Ensuring expected distances are returned for each distance queryset. # 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): for i, c in enumerate(qs):
self.assertAlmostEqual(m_distances[i], c.distance.m, tol) self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
@no_spatialite
def test03b_distance_method(self): def test03b_distance_method(self):
"Testing the `distance` GeoQuerySet method on geodetic coordnate systems." "Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
if oracle: tol = 2 if oracle: tol = 2
@ -139,8 +152,8 @@ class DistanceTest(unittest.TestCase):
if not oracle: if not oracle:
# PostGIS is limited to disance queries only to/from point geometries, # PostGIS is limited to disance queries only to/from point geometries,
# ensuring a TypeError is raised if something else is put in. # ensuring a TypeError is raised if something else is put in.
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)') self.assertRaises(ValueError, 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)))
# Got the reference distances using the raw SQL statements: # 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)); # 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,7 +176,7 @@ class DistanceTest(unittest.TestCase):
"Testing the `distance` GeoQuerySet method used with `transform` on a geographic field." "Testing the `distance` GeoQuerySet method used with `transform` on a geographic field."
# Normally you can't compute distances from a geometry field # Normally you can't compute distances from a geometry field
# that is not a PointField (on PostGIS). # 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 # We'll be using a Polygon (created by buffering the centroid
# of 77005 to 100m) -- which aren't allowed in geographic distance # of 77005 to 100m) -- which aren't allowed in geographic distance
@ -182,9 +195,11 @@ class DistanceTest(unittest.TestCase):
# however. # however.
buf1 = z.poly.centroid.buffer(100) buf1 = z.poly.centroid.buffer(100)
buf2 = buf1.transform(4269, clone=True) buf2 = buf1.transform(4269, clone=True)
ref_zips = ['77002', '77025', '77401']
for buf in [buf1, buf2]: for buf in [buf1, buf2]:
qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf) 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): for i, z in enumerate(qs):
self.assertAlmostEqual(z.distance.m, dists_m[i], 5) 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 # (thus, Houston and Southside place will be excluded as tested in
# the `test02_dwithin` above). # 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))) qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
# 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))) 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: dist_qs = (qs1, qs2)
for qs in dist_qs:
cities = self.get_names(qs) cities = self.get_names(qs)
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
@ -207,6 +230,7 @@ class DistanceTest(unittest.TestCase):
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300))) qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300)))
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
@no_spatialite
def test05_geodetic_distance_lookups(self): def test05_geodetic_distance_lookups(self):
"Testing distance lookups on geodetic coordinate systems." "Testing distance lookups on geodetic coordinate systems."
if not oracle: if not oracle:
@ -235,14 +259,13 @@ class DistanceTest(unittest.TestCase):
d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles. d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles.
# Normal geodetic distance lookup (uses `distance_sphere` on PostGIS. # Normal geodetic distance lookup (uses `distance_sphere` on PostGIS.
gq1 = GeoQ(point__distance_lte=(wollongong.point, d1)) gq1 = Q(point__distance_lte=(wollongong.point, d1))
gq2 = GeoQ(point__distance_gte=(wollongong.point, d2)) gq2 = Q(point__distance_gte=(wollongong.point, d2))
qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2) qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
# Geodetic distance lookup but telling GeoDjango to use `distance_spheroid` # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid`
# instead (we should get the same results b/c accuracy variance won't matter # 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 # in this test case).
# it doesn't matter).
if postgis: if postgis:
gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid')) gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid')) gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
@ -270,12 +293,23 @@ class DistanceTest(unittest.TestCase):
"Testing the `length` GeoQuerySet method." "Testing the `length` GeoQuerySet method."
# Reference query (should use `length_spheroid`). # Reference query (should use `length_spheroid`).
# SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]'); # SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]');
len_m = 473504.769553813 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() qs = Interstate.objects.length()
if oracle: tol = 2 if oracle: tol = 2
else: tol = 5 else: tol = 5
self.assertAlmostEqual(len_m, qs[0].length.m, tol) 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): def test08_perimeter(self):
"Testing the `perimeter` GeoQuerySet method." "Testing the `perimeter` GeoQuerySet method."
# Reference query: # Reference query:

View File

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

View File

@ -1,10 +1,13 @@
import os, unittest import os, unittest
from models import Country, City, PennsylvaniaCity, State, Feature, MinusOneSRID
from django.contrib.gis import gdal from django.contrib.gis import gdal
from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.geos import * from django.contrib.gis.geos import *
from django.contrib.gis.measure import Distance 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 # 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; # 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(2, Country.objects.count())
self.assertEqual(8, City.objects.count()) self.assertEqual(8, City.objects.count())
# Oracle cannot handle NULL geometry values w/certain queries. # Only PostGIS can handle NULL geometries
if SpatialBackend.oracle: n_state = 2 if SpatialBackend.postgis or SpatialBackend.spatialite:
else: n_state = 3 n_state = 3
else:
n_state = 2
self.assertEqual(n_state, State.objects.count()) self.assertEqual(n_state, State.objects.count())
def test02_proxy(self): def test02_proxy(self):
@ -112,6 +117,7 @@ class GeoModelTest(unittest.TestCase):
ns.delete() ns.delete()
@no_oracle # Oracle does not support KML. @no_oracle # Oracle does not support KML.
@no_spatialite # SpatiaLite does not support KML.
def test03a_kml(self): def test03a_kml(self):
"Testing KML output from the database using GeoManager.kml()." "Testing KML output from the database using GeoManager.kml()."
if DISABLE: return if DISABLE: return
@ -137,6 +143,7 @@ class GeoModelTest(unittest.TestCase):
for ptown in [ptown1, ptown2]: for ptown in [ptown1, ptown2]:
self.assertEqual(ref_kml, ptown.kml) self.assertEqual(ref_kml, ptown.kml)
@no_spatialite # SpatiaLite does not support GML.
def test03b_gml(self): def test03b_gml(self):
"Testing GML output from the database using GeoManager.gml()." "Testing GML output from the database using GeoManager.gml()."
if DISABLE: return if DISABLE: return
@ -150,7 +157,7 @@ class GeoModelTest(unittest.TestCase):
if SpatialBackend.oracle: if SpatialBackend.oracle:
# No precision parameter for Oracle :-/ # No precision parameter for Oracle :-/
import re 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]: for ptown in [ptown1, ptown2]:
self.assertEqual(True, bool(gml_regex.match(ptown.gml))) self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
else: else:
@ -180,7 +187,7 @@ class GeoModelTest(unittest.TestCase):
self.assertAlmostEqual(ptown.x, p.point.x, prec) self.assertAlmostEqual(ptown.x, p.point.x, prec)
self.assertAlmostEqual(ptown.y, p.point.y, 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): def test05_extent(self):
"Testing the `extent` GeoQuerySet method." "Testing the `extent` GeoQuerySet method."
if DISABLE: return if DISABLE: return
@ -193,9 +200,10 @@ class GeoModelTest(unittest.TestCase):
extent = qs.extent() extent = qs.extent()
for val, exp in zip(extent, expected): for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 8) self.assertAlmostEqual(exp, val, 4)
@no_oracle @no_oracle
@no_spatialite # SpatiaLite does not have a MakeLine function
def test06_make_line(self): def test06_make_line(self):
"Testing the `make_line` GeoQuerySet method." "Testing the `make_line` GeoQuerySet method."
if DISABLE: return if DISABLE: return
@ -214,10 +222,13 @@ class GeoModelTest(unittest.TestCase):
qs1 = City.objects.filter(point__disjoint=ptown.point) qs1 = City.objects.filter(point__disjoint=ptown.point)
self.assertEqual(7, qs1.count()) 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 # TODO: Do NULL columns bork queries on PostGIS? The following
# error is encountered: # error is encountered:
# psycopg2.ProgrammingError: invalid memory alloc request size 4294957297 # 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) qs2 = State.objects.filter(poly__disjoint=ptown.point)
self.assertEqual(1, qs2.count()) self.assertEqual(1, qs2.count())
self.assertEqual('Kansas', qs2[0].name) self.assertEqual('Kansas', qs2[0].name)
@ -248,9 +259,12 @@ class GeoModelTest(unittest.TestCase):
# Houston and Wellington. # Houston and Wellington.
tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry
nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX 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('Texas', tx.name)
self.assertEqual('New Zealand', nz.name) self.assertEqual('New Zealand', nz.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) self.assertEqual('Kansas', ks.name)
# Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas) # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
@ -304,6 +318,8 @@ class GeoModelTest(unittest.TestCase):
# If the GeometryField SRID is -1, then we shouldn't perform any # If the GeometryField SRID is -1, then we shouldn't perform any
# transformation if the SRID of the input geometry is different. # transformation if the SRID of the input geometry is different.
# SpatiaLite does not support missing SRID values.
if not SpatialBackend.spatialite:
m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
m1.save() m1.save()
self.assertEqual(-1, m1.geom.srid) self.assertEqual(-1, m1.geom.srid)
@ -311,6 +327,7 @@ class GeoModelTest(unittest.TestCase):
# Oracle does not support NULL geometries in its spatial index for # Oracle does not support NULL geometries in its spatial index for
# some routines (e.g., SDO_GEOM.RELATE). # some routines (e.g., SDO_GEOM.RELATE).
@no_oracle @no_oracle
@no_spatialite
def test12_null_geometries(self): def test12_null_geometries(self):
"Testing NULL geometry support, and the `isnull` lookup type." "Testing NULL geometry support, and the `isnull` lookup type."
if DISABLE: return if DISABLE: return
@ -334,6 +351,7 @@ class GeoModelTest(unittest.TestCase):
State(name='Northern Mariana Islands', poly=None).save() State(name='Northern Mariana Islands', poly=None).save()
@no_oracle # No specific `left` or `right` operators in Oracle. @no_oracle # No specific `left` or `right` operators in Oracle.
@no_spatialite # No `left` or `right` operators in SpatiaLite.
def test13_left_right(self): def test13_left_right(self):
"Testing the 'left' and 'right' lookup types." "Testing the 'left' and 'right' lookup types."
if DISABLE: return if DISABLE: return
@ -398,7 +416,7 @@ class GeoModelTest(unittest.TestCase):
self.assertRaises(e, qs.count) self.assertRaises(e, qs.count)
# Relate works differently for the different backends. # Relate works differently for the different backends.
if SpatialBackend.postgis: if SpatialBackend.postgis or SpatialBackend.spatialite:
contains_mask = 'T*T***FF*' contains_mask = 'T*T***FF*'
within_mask = 'T*F**F***' within_mask = 'T*F**F***'
intersects_mask = 'T********' intersects_mask = 'T********'
@ -449,9 +467,12 @@ class GeoModelTest(unittest.TestCase):
union = union1 union = union1
self.assertEqual(True, union.equals_exact(u1, tol)) self.assertEqual(True, union.equals_exact(u1, tol))
self.assertEqual(True, union.equals_exact(u2, tol)) self.assertEqual(True, union.equals_exact(u2, tol))
# SpatiaLite will segfault trying to union a NULL geometry.
if not SpatialBackend.spatialite:
qs = City.objects.filter(name='NotACity') qs = City.objects.filter(name='NotACity')
self.assertEqual(None, qs.unionagg(field_name='point')) self.assertEqual(None, qs.unionagg(field_name='point'))
@no_spatialite # SpatiaLite does not support abstract geometry columns
def test18_geometryfield(self): def test18_geometryfield(self):
"Testing GeometryField." "Testing GeometryField."
if DISABLE: return if DISABLE: return
@ -479,8 +500,12 @@ class GeoModelTest(unittest.TestCase):
"Testing the `centroid` GeoQuerySet method." "Testing the `centroid` GeoQuerySet method."
if DISABLE: return if DISABLE: return
qs = State.objects.exclude(poly__isnull=True).centroid() qs = State.objects.exclude(poly__isnull=True).centroid()
if SpatialBackend.oracle: tol = 0.1 if SpatialBackend.oracle:
else: tol = 0.000000001 tol = 0.1
elif SpatialBackend.spatialite:
tol = 0.000001
else:
tol = 0.000000001
for s in qs: for s in qs:
self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol)) 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), ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
'Texas' : fromstr('POINT (-103.002434 36.500397)', 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 # Using GEOSGeometry to compute the reference point on surface values
# -- since PostGIS also uses GEOS these should be the same. # -- since PostGIS also uses GEOS these should be the same.
ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface, ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface,
'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface 'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface
} }
for cntry in Country.objects.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 @no_oracle
def test21_scale(self): def test21_scale(self):
@ -512,8 +542,9 @@ class GeoModelTest(unittest.TestCase):
for p1, p2 in zip(c.mpoly, c.scaled): for p1, p2 in zip(c.mpoly, c.scaled):
for r1, r2 in zip(p1, p2): for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords): for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] * xfac, c2[0]) # XXX The low precision is for SpatiaLite
self.assertEqual(c1[1] * yfac, c2[1]) self.assertAlmostEqual(c1[0] * xfac, c2[0], 5)
self.assertAlmostEqual(c1[1] * yfac, c2[1], 5)
@no_oracle @no_oracle
def test22_translate(self): def test22_translate(self):
@ -525,8 +556,9 @@ class GeoModelTest(unittest.TestCase):
for p1, p2 in zip(c.mpoly, c.translated): for p1, p2 in zip(c.mpoly, c.translated):
for r1, r2 in zip(p1, p2): for r1, r2 in zip(p1, p2):
for c1, c2 in zip(r1.coords, r2.coords): for c1, c2 in zip(r1.coords, r2.coords):
self.assertEqual(c1[0] + xfac, c2[0]) # XXX The low precision is for SpatiaLite
self.assertEqual(c1[1] + yfac, c2[1]) self.assertAlmostEqual(c1[0] + xfac, c2[0], 5)
self.assertAlmostEqual(c1[1] + yfac, c2[1], 5)
def test23_numgeom(self): def test23_numgeom(self):
"Testing the `num_geom` GeoQuerySet method." "Testing the `num_geom` GeoQuerySet method."
@ -539,27 +571,46 @@ class GeoModelTest(unittest.TestCase):
if SpatialBackend.postgis: self.assertEqual(None, c.num_geom) if SpatialBackend.postgis: self.assertEqual(None, c.num_geom)
else: self.assertEqual(1, c.num_geom) else: self.assertEqual(1, c.num_geom)
@no_spatialite # SpatiaLite can only count vertices in LineStrings
def test24_numpoints(self): def test24_numpoints(self):
"Testing the `num_points` GeoQuerySet method." "Testing the `num_points` GeoQuerySet method."
if DISABLE: return if DISABLE: return
for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points) for c in Country.objects.num_points():
if SpatialBackend.postgis: self.assertEqual(c.mpoly.num_points, c.num_points)
if not SpatialBackend.oracle:
# Oracle cannot count vertices in Point geometries. # Oracle cannot count vertices in Point geometries.
for c in City.objects.num_points(): self.assertEqual(1, c.num_points) for c in City.objects.num_points(): self.assertEqual(1, c.num_points)
@no_oracle
def test25_geoset(self): def test25_geoset(self):
"Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
if DISABLE: return if DISABLE: return
geom = Point(5, 23) geom = Point(5, 23)
for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom): 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) self.assertEqual(c.mpoly.difference(geom), c.difference)
if not SpatialBackend.spatialite:
self.assertEqual(c.mpoly.intersection(geom), c.intersection) self.assertEqual(c.mpoly.intersection(geom), c.intersection)
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference) self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
self.assertEqual(c.mpoly.union(geom), c.union) self.assertEqual(c.mpoly.union(geom), c.union)
def test26_inherited_geofields(self): def test26_inherited_geofields(self):
"Test GeoQuerySet methods on inherited Geometry fields." "Test GeoQuerySet methods on inherited Geometry fields."
if DISABLE: return
# Creating a Pennsylvanian city. # Creating a Pennsylvanian city.
mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)') 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 from django.contrib.gis.gdal import DataSource
shp_path = os.path.dirname(__file__) shp_path = os.path.dirname(__file__)
city_shp = os.path.join(shp_path, 'cities/cities.shp') city_shp = os.path.join(shp_path, '../data/cities/cities.shp')
co_shp = os.path.join(shp_path, 'counties/counties.shp') co_shp = os.path.join(shp_path, '../data/counties/counties.shp')
inter_shp = os.path.join(shp_path, 'interstates/interstates.shp') inter_shp = os.path.join(shp_path, '../data/interstates/interstates.shp')
# Dictionaries to hold what's expected in the county shapefile. # Dictionaries to hold what's expected in the county shapefile.
NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo']
@ -53,7 +53,6 @@ class LayerMapTest(unittest.TestCase):
def test02_simple_layermap(self): def test02_simple_layermap(self):
"Test LayerMapping import of a simple point shapefile." "Test LayerMapping import of a simple point shapefile."
# Setting up for the LayerMapping. # Setting up for the LayerMapping.
lm = LayerMapping(City, city_shp, city_mapping) lm = LayerMapping(City, city_shp, city_mapping)
lm.save() lm.save()
@ -78,7 +77,6 @@ class LayerMapTest(unittest.TestCase):
def test03_layermap_strict(self): def test03_layermap_strict(self):
"Testing the `strict` keyword, and import of a LineString shapefile." "Testing the `strict` keyword, and import of a LineString shapefile."
# When the `strict` keyword is set an error encountered will force # When the `strict` keyword is set an error encountered will force
# the importation to stop. # the importation to stop.
try: try:
@ -118,7 +116,6 @@ class LayerMapTest(unittest.TestCase):
def county_helper(self, county_feat=True): def county_helper(self, county_feat=True):
"Helper function for ensuring the integrity of the mapped County models." "Helper function for ensuring the integrity of the mapped County models."
for name, n, st in zip(NAMES, NUMS, STATES): for name, n, st in zip(NAMES, NUMS, STATES):
# Should only be one record b/c of `unique` keyword. # Should only be one record b/c of `unique` keyword.
c = County.objects.get(name=name) c = County.objects.get(name=name)
@ -198,7 +195,6 @@ class LayerMapTest(unittest.TestCase):
def test05_test_fid_range_step(self): def test05_test_fid_range_step(self):
"Tests the `fid_range` keyword and the `step` keyword of .save()." "Tests the `fid_range` keyword and the `step` keyword of .save()."
# Function for clearing out all the counties before testing. # Function for clearing out all the counties before testing.
def clear_counties(): County.objects.all().delete() 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.geos import *
from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models import F, Extent, Union 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 django.conf import settings
from models import City, Location, DirectoryEntry, Parcel from models import City, Location, DirectoryEntry, Parcel
@ -62,11 +62,11 @@ class RelatedGeoModelTest(unittest.TestCase):
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
@no_mysql @no_mysql
def test04_related_aggregate(self): @no_spatialite
"Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models." 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 # 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. # One for all locations, one that excludes Roswell.
all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633) all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
@ -81,6 +81,12 @@ class RelatedGeoModelTest(unittest.TestCase):
for ref, e in [(all_extent, e1), (txpa_extent, e2), (all_extent, e3)]: 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) 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 # These are the points that are components of the aggregate geographic
# union that is returned. # union that is returned.
p1 = Point(-104.528056, 33.387222) p1 = Point(-104.528056, 33.387222)

View File

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