From 867e71501c3273ba4a0b3ac13847cd895f0da663 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Mon, 30 Mar 2009 17:15:49 +0000 Subject: [PATCH] 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 --- django/contrib/gis/db/backend/__init__.py | 8 +- django/contrib/gis/db/backend/base.py | 5 +- .../contrib/gis/db/backend/mysql/__init__.py | 4 +- .../contrib/gis/db/backend/mysql/creation.py | 4 +- django/contrib/gis/db/backend/mysql/field.py | 24 +-- .../contrib/gis/db/backend/oracle/__init__.py | 7 +- .../contrib/gis/db/backend/oracle/creation.py | 3 +- django/contrib/gis/db/backend/oracle/field.py | 53 +++--- .../contrib/gis/db/backend/oracle/models.py | 13 +- .../gis/db/backend/postgis/__init__.py | 8 +- .../gis/db/backend/postgis/creation.py | 27 +-- .../contrib/gis/db/backend/postgis/field.py | 58 +++--- .../contrib/gis/db/backend/postgis/models.py | 13 +- .../contrib/gis/db/backend/postgis/query.py | 1 + django/contrib/gis/db/backend/util.py | 21 +- django/contrib/gis/db/models/__init__.py | 3 - django/contrib/gis/db/models/aggregates.py | 5 +- .../contrib/gis/db/models/fields/__init__.py | 64 +++++-- django/contrib/gis/db/models/mixin.py | 11 -- django/contrib/gis/db/models/proxy.py | 4 +- django/contrib/gis/db/models/query.py | 67 +++++-- .../contrib/gis/db/models/sql/aggregates.py | 17 +- .../contrib/gis/db/models/sql/conversion.py | 4 + django/contrib/gis/db/models/sql/query.py | 11 +- django/contrib/gis/db/models/sql/where.py | 10 +- django/contrib/gis/forms/fields.py | 1 + django/contrib/gis/models.py | 180 ++++++++++-------- django/contrib/gis/tests/__init__.py | 73 +++---- .../{layermap => data}/cities/cities.dbf | Bin .../{layermap => data}/cities/cities.prj | 0 .../{layermap => data}/cities/cities.shp | Bin .../{layermap => data}/cities/cities.shx | Bin .../{layermap => data}/counties/counties.dbf | Bin .../{layermap => data}/counties/counties.shp | Bin .../{layermap => data}/counties/counties.shx | Bin .../interstates/interstates.dbf | Bin .../interstates/interstates.prj | 0 .../interstates/interstates.shp | Bin .../interstates/interstates.shx | Bin django/contrib/gis/tests/distapp/data.py | 3 + django/contrib/gis/tests/distapp/models.py | 9 +- django/contrib/gis/tests/distapp/tests.py | 108 +++++++---- django/contrib/gis/tests/geoapp/models.py | 21 +- django/contrib/gis/tests/geoapp/tests.py | 117 ++++++++---- django/contrib/gis/tests/layermap/tests.py | 10 +- django/contrib/gis/tests/relatedapp/tests.py | 36 ++-- django/contrib/gis/tests/utils.py | 2 + 47 files changed, 595 insertions(+), 410 deletions(-) delete mode 100644 django/contrib/gis/db/models/mixin.py rename django/contrib/gis/tests/{layermap => data}/cities/cities.dbf (100%) rename django/contrib/gis/tests/{layermap => data}/cities/cities.prj (100%) rename django/contrib/gis/tests/{layermap => data}/cities/cities.shp (100%) rename django/contrib/gis/tests/{layermap => data}/cities/cities.shx (100%) rename django/contrib/gis/tests/{layermap => data}/counties/counties.dbf (100%) rename django/contrib/gis/tests/{layermap => data}/counties/counties.shp (100%) rename django/contrib/gis/tests/{layermap => data}/counties/counties.shx (100%) rename django/contrib/gis/tests/{layermap => data}/interstates/interstates.dbf (100%) rename django/contrib/gis/tests/{layermap => data}/interstates/interstates.prj (100%) rename django/contrib/gis/tests/{layermap => data}/interstates/interstates.shp (100%) rename django/contrib/gis/tests/{layermap => data}/interstates/interstates.shx (100%) diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index 172c1268a78..adee0bff675 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -9,10 +9,12 @@ from django.contrib.gis.db.backend.util import gqn # Retrieving the necessary settings from the backend. if settings.DATABASE_ENGINE == 'postgresql_psycopg2': - from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend + from django.contrib.gis.db.backend.postgis import create_test_spatial_db, get_geo_where_clause, SpatialBackend elif settings.DATABASE_ENGINE == 'oracle': - from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend + from django.contrib.gis.db.backend.oracle import create_test_spatial_db, get_geo_where_clause, SpatialBackend elif settings.DATABASE_ENGINE == 'mysql': - from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend + from django.contrib.gis.db.backend.mysql import create_test_spatial_db, get_geo_where_clause, SpatialBackend +elif settings.DATABASE_ENGINE == 'sqlite3': + from django.contrib.gis.db.backend.spatialite import create_test_spatial_db, get_geo_where_clause, SpatialBackend else: raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) diff --git a/django/contrib/gis/db/backend/base.py b/django/contrib/gis/db/backend/base.py index d45ac7b6f1d..bffb972670f 100644 --- a/django/contrib/gis/db/backend/base.py +++ b/django/contrib/gis/db/backend/base.py @@ -23,7 +23,4 @@ class BaseSpatialBackend(object): return self.__dict__[name] except KeyError: return False - - - - + diff --git a/django/contrib/gis/db/backend/mysql/__init__.py b/django/contrib/gis/db/backend/mysql/__init__.py index 0484e5f9b2e..9838cb3983f 100644 --- a/django/contrib/gis/db/backend/mysql/__init__.py +++ b/django/contrib/gis/db/backend/mysql/__init__.py @@ -1,8 +1,8 @@ -__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] +__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] from django.contrib.gis.db.backend.base import BaseSpatialBackend from django.contrib.gis.db.backend.adaptor import WKTAdaptor -from django.contrib.gis.db.backend.mysql.creation import create_spatial_db +from django.contrib.gis.db.backend.mysql.creation import create_test_spatial_db from django.contrib.gis.db.backend.mysql.field import MySQLGeoField from django.contrib.gis.db.backend.mysql.query import * diff --git a/django/contrib/gis/db/backend/mysql/creation.py b/django/contrib/gis/db/backend/mysql/creation.py index 3da21a0cdd7..f55fdac5df8 100644 --- a/django/contrib/gis/db/backend/mysql/creation.py +++ b/django/contrib/gis/db/backend/mysql/creation.py @@ -1,5 +1,5 @@ -def create_spatial_db(test=True, verbosity=1, autoclobber=False): - if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py') +def create_test_spatial_db(verbosity=1, autoclobber=False): + "A wrapper over the MySQL `create_test_db` method." from django.db import connection connection.creation.create_test_db(verbosity, autoclobber) diff --git a/django/contrib/gis/db/backend/mysql/field.py b/django/contrib/gis/db/backend/mysql/field.py index 3ad9a1c761a..e5c22f58e63 100644 --- a/django/contrib/gis/db/backend/mysql/field.py +++ b/django/contrib/gis/db/backend/mysql/field.py @@ -13,20 +13,20 @@ class MySQLGeoField(Field): def _geom_index(self, style, db_table): """ Creates a spatial index for the geometry column. If MyISAM tables are - used an R-Tree index is created, otherwise a B-Tree index is created. + used an R-Tree index is created, otherwise a B-Tree index is created. Thus, for best spatial performance, you should use MyISAM tables - (which do not support transactions). For more information, see Ch. + (which do not support transactions). For more information, see Ch. 16.6.1 of the MySQL 5.0 documentation. """ # Getting the index name. idx_name = '%s_%s_id' % (db_table, self.column) - - sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \ - style.SQL_TABLE(qn(idx_name)) + \ - style.SQL_KEYWORD(' ON ') + \ - style.SQL_TABLE(qn(db_table)) + '(' + \ - style.SQL_FIELD(qn(self.column)) + ');' + + sql = (style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + + style.SQL_TABLE(qn(idx_name)) + + style.SQL_KEYWORD(' ON ') + + style.SQL_TABLE(qn(db_table)) + '(' + + style.SQL_FIELD(qn(self.column)) + ');') return sql def post_create_sql(self, style, db_table): @@ -35,19 +35,19 @@ class MySQLGeoField(Field): created. """ # Getting the geometric index for this Geometry column. - if self._index: + if self.spatial_index: return (self._geom_index(style, db_table),) else: return () def db_type(self): "The OpenGIS name is returned for the MySQL database column type." - return self._geom + return self.geom_type def get_placeholder(self, value): """ - The placeholder here has to include MySQL's WKT constructor. Because - MySQL does not support spatial transformations, there is no need to + The placeholder here has to include MySQL's WKT constructor. Because + MySQL does not support spatial transformations, there is no need to modify the placeholder based on the contents of the given value. """ return '%s(%%s)' % GEOM_FROM_TEXT diff --git a/django/contrib/gis/db/backend/oracle/__init__.py b/django/contrib/gis/db/backend/oracle/__init__.py index 22402103faa..9f25214e023 100644 --- a/django/contrib/gis/db/backend/oracle/__init__.py +++ b/django/contrib/gis/db/backend/oracle/__init__.py @@ -1,9 +1,10 @@ -__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] +__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] from django.contrib.gis.db.backend.base import BaseSpatialBackend from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor -from django.contrib.gis.db.backend.oracle.creation import create_spatial_db +from django.contrib.gis.db.backend.oracle.creation import create_test_spatial_db from django.contrib.gis.db.backend.oracle.field import OracleSpatialField +from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys from django.contrib.gis.db.backend.oracle.query import * SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True, @@ -29,4 +30,6 @@ SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True, union=UNION, Adaptor=OracleSpatialAdaptor, Field=OracleSpatialField, + GeometryColumns=GeometryColumns, + SpatialRefSys=SpatialRefSys, ) diff --git a/django/contrib/gis/db/backend/oracle/creation.py b/django/contrib/gis/db/backend/oracle/creation.py index d9b53d20496..a1ea56f3c9e 100644 --- a/django/contrib/gis/db/backend/oracle/creation.py +++ b/django/contrib/gis/db/backend/oracle/creation.py @@ -1,6 +1,5 @@ -def create_spatial_db(test=True, verbosity=1, autoclobber=False): +def create_test_spatial_db(verbosity=1, autoclobber=False): "A wrapper over the Oracle `create_test_db` routine." - if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py') from django.db import connection connection.creation.create_test_db(verbosity, autoclobber) diff --git a/django/contrib/gis/db/backend/oracle/field.py b/django/contrib/gis/db/backend/oracle/field.py index 22625f5e10c..4835f44a126 100644 --- a/django/contrib/gis/db/backend/oracle/field.py +++ b/django/contrib/gis/db/backend/oracle/field.py @@ -32,26 +32,25 @@ class OracleSpatialField(Field): Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA table. """ - # Checking the dimensions. # TODO: Add support for 3D geometries. - if self._dim != 2: + if self.dim != 2: raise Exception('3D geometries not yet supported on Oracle Spatial backend.') # Constructing the SQL that will be used to insert information about # the geometry column into the USER_GSDO_GEOM_METADATA table. - meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \ - style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \ - ' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \ - style.SQL_KEYWORD(' VALUES ') + '(\n ' + \ - style.SQL_TABLE(gqn(db_table)) + ',\n ' + \ - style.SQL_FIELD(gqn(self.column)) + ',\n ' + \ - style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \ - style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ - ("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \ - style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ - ("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \ - ' %s\n );' % self._srid + meta_sql = (style.SQL_KEYWORD('INSERT INTO ') + + style.SQL_TABLE('USER_SDO_GEOM_METADATA') + + ' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + + style.SQL_KEYWORD(' VALUES ') + '(\n ' + + style.SQL_TABLE(gqn(db_table)) + ',\n ' + + style.SQL_FIELD(gqn(self.column)) + ',\n ' + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + + ("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + + ("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + + ' %s\n );' % self.srid) return meta_sql def _geom_index(self, style, db_table): @@ -60,14 +59,14 @@ class OracleSpatialField(Field): # Getting the index name, Oracle doesn't allow object # names > 30 characters. idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30) - - sql = style.SQL_KEYWORD('CREATE INDEX ') + \ - style.SQL_TABLE(qn(idx_name)) + \ - style.SQL_KEYWORD(' ON ') + \ - style.SQL_TABLE(qn(db_table)) + '(' + \ - style.SQL_FIELD(qn(self.column)) + ') ' + \ - style.SQL_KEYWORD('INDEXTYPE IS ') + \ - style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';' + + sql = (style.SQL_KEYWORD('CREATE INDEX ') + + style.SQL_TABLE(qn(idx_name)) + + style.SQL_KEYWORD(' ON ') + + style.SQL_TABLE(qn(db_table)) + '(' + + style.SQL_FIELD(qn(self.column)) + ') ' + + style.SQL_KEYWORD('INDEXTYPE IS ') + + style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';') return sql def post_create_sql(self, style, db_table): @@ -79,7 +78,7 @@ class OracleSpatialField(Field): post_sql = self._add_geom(style, db_table) # Getting the geometric index for this Geometry column. - if self._index: + if self.spatial_index: return (post_sql, self._geom_index(style, db_table)) else: return (post_sql,) @@ -87,7 +86,7 @@ class OracleSpatialField(Field): def db_type(self): "The Oracle geometric data type is MDSYS.SDO_GEOMETRY." return 'MDSYS.SDO_GEOMETRY' - + def get_placeholder(self, value): """ Provides a proper substitution value for Geometries that are not in the @@ -96,8 +95,8 @@ class OracleSpatialField(Field): """ if value is None: return '%s' - elif value.srid != self._srid: + elif value.srid != self.srid: # Adding Transform() to the SQL placeholder. - return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid) + return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self.srid) else: - return 'SDO_GEOMETRY(%%s, %s)' % self._srid + return 'SDO_GEOMETRY(%%s, %s)' % self.srid diff --git a/django/contrib/gis/db/backend/oracle/models.py b/django/contrib/gis/db/backend/oracle/models.py index d8d00d402d4..2854b106219 100644 --- a/django/contrib/gis/db/backend/oracle/models.py +++ b/django/contrib/gis/db/backend/oracle/models.py @@ -8,7 +8,6 @@ model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model. """ from django.db import models -from django.contrib.gis.models import SpatialRefSysMixin class GeometryColumns(models.Model): "Maps to the Oracle USER_SDO_GEOM_METADATA table." @@ -22,7 +21,7 @@ class GeometryColumns(models.Model): @classmethod def table_name_col(cls): """ - Returns the name of the metadata column used to store the + Returns the name of the metadata column used to store the the feature table name. """ return 'table_name' @@ -30,7 +29,7 @@ class GeometryColumns(models.Model): @classmethod def geom_col_name(cls): """ - Returns the name of the metadata column used to store the + Returns the name of the metadata column used to store the the feature geometry column. """ return 'column_name' @@ -38,19 +37,19 @@ class GeometryColumns(models.Model): def __unicode__(self): return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid) -class SpatialRefSys(models.Model, SpatialRefSysMixin): +class SpatialRefSys(models.Model): "Maps to the Oracle MDSYS.CS_SRS table." cs_name = models.CharField(max_length=68) srid = models.IntegerField(primary_key=True) auth_srid = models.IntegerField() auth_name = models.CharField(max_length=256) wktext = models.CharField(max_length=2046) - #cs_bounds = models.GeometryField() + #cs_bounds = models.GeometryField() # TODO class Meta: - # TODO: Figure out way to have this be MDSYS.CS_SRS without - # having django's quoting mess up the SQL. + abstract = True db_table = 'CS_SRS' + app_label = '_mdsys' # Hack so that syncdb won't try to create "CS_SRS" table. @property def wkt(self): diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 8a4d09e0d55..1c45363136b 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -1,14 +1,16 @@ -__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] +__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] from django.contrib.gis.db.backend.base import BaseSpatialBackend from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor -from django.contrib.gis.db.backend.postgis.creation import create_spatial_db +from django.contrib.gis.db.backend.postgis.creation import create_test_spatial_db from django.contrib.gis.db.backend.postgis.field import PostGISField +from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys from django.contrib.gis.db.backend.postgis.query import * SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, area=AREA, centroid=CENTROID, + collect=COLLECT, difference=DIFFERENCE, distance=DISTANCE, distance_functions=DISTANCE_FUNCTIONS, @@ -39,4 +41,6 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2), Adaptor=PostGISAdaptor, Field=PostGISField, + GeometryColumns=GeometryColumns, + SpatialRefSys=SpatialRefSys, ) diff --git a/django/contrib/gis/db/backend/postgis/creation.py b/django/contrib/gis/db/backend/postgis/creation.py index 113416de382..bb7438091d3 100644 --- a/django/contrib/gis/db/backend/postgis/creation.py +++ b/django/contrib/gis/db/backend/postgis/creation.py @@ -1,23 +1,10 @@ import os, re, sys -from subprocess import Popen, PIPE from django.conf import settings from django.core.management import call_command from django.db import connection from django.db.backends.creation import TEST_DATABASE_PREFIX - -def getstatusoutput(cmd): - """ - Executes a shell command on the platform using subprocess.Popen and - return a tuple of the status and stdout output. - """ - # Set stdout and stderr to PIPE because we want to capture stdout and - # prevent stderr from displaying. - p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) - # We use p.communicate() instead of p.wait() to avoid deadlocks if the - # output buffers exceed POSIX buffer size. - stdout, stderr = p.communicate() - return p.returncode, stdout.strip() +from django.contrib.gis.db.backend.util import getstatusoutput def create_lang(db_name, verbosity=1): "Sets up the pl/pgsql language on the given database." @@ -110,20 +97,16 @@ def _create_with_shell(db_name, verbosity=1, autoclobber=False): else: raise Exception('Unknown error occurred in creating database: %s' % output) -def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): - "Creates a spatial database based on the settings." +def create_test_spatial_db(verbosity=1, autoclobber=False, interactive=False): + "Creates a test spatial database based on the settings." # Making sure we're using PostgreSQL and psycopg2 if settings.DATABASE_ENGINE != 'postgresql_psycopg2': raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.') # Getting the spatial database name - if test: - db_name = get_spatial_db(test=True) - _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) - else: - db_name = get_spatial_db() - _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber) + db_name = get_spatial_db(test=True) + _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) # If a template database is used, then don't need to do any of the following. if not hasattr(settings, 'POSTGIS_TEMPLATE'): diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py index 9d6c0fad24c..e08bda3d4cb 100644 --- a/django/contrib/gis/db/backend/postgis/field.py +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -19,35 +19,35 @@ class PostGISField(Field): Takes the style object (provides syntax highlighting) and the database table as parameters. """ - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_TABLE('AddGeometryColumn') + '(' + \ - style.SQL_TABLE(gqn(db_table)) + ', ' + \ - style.SQL_FIELD(gqn(self.column)) + ', ' + \ - style.SQL_FIELD(str(self._srid)) + ', ' + \ - style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \ - style.SQL_KEYWORD(str(self._dim)) + ');' + sql = (style.SQL_KEYWORD('SELECT ') + + style.SQL_TABLE('AddGeometryColumn') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(self.column)) + ', ' + + style.SQL_FIELD(str(self.srid)) + ', ' + + style.SQL_COLTYPE(gqn(self.geom_type)) + ', ' + + style.SQL_KEYWORD(str(self.dim)) + ');') if not self.null: # Add a NOT NULL constraint to the field - sql += '\n' + \ - style.SQL_KEYWORD('ALTER TABLE ') + \ - style.SQL_TABLE(qn(db_table)) + \ - style.SQL_KEYWORD(' ALTER ') + \ - style.SQL_FIELD(qn(self.column)) + \ - style.SQL_KEYWORD(' SET NOT NULL') + ';' + sql += ('\n' + + style.SQL_KEYWORD('ALTER TABLE ') + + style.SQL_TABLE(qn(db_table)) + + style.SQL_KEYWORD(' ALTER ') + + style.SQL_FIELD(qn(self.column)) + + style.SQL_KEYWORD(' SET NOT NULL') + ';') return sql - + def _geom_index(self, style, db_table, index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): "Creates a GiST index for this geometry field." - sql = style.SQL_KEYWORD('CREATE INDEX ') + \ - style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \ - style.SQL_KEYWORD(' ON ') + \ - style.SQL_TABLE(qn(db_table)) + \ - style.SQL_KEYWORD(' USING ') + \ - style.SQL_COLTYPE(index_type) + ' ( ' + \ - style.SQL_FIELD(qn(self.column)) + ' ' + \ - style.SQL_KEYWORD(index_opts) + ' );' + sql = (style.SQL_KEYWORD('CREATE INDEX ') + + style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + + style.SQL_KEYWORD(' ON ') + + style.SQL_TABLE(qn(db_table)) + + style.SQL_KEYWORD(' USING ') + + style.SQL_COLTYPE(index_type) + ' ( ' + + style.SQL_FIELD(qn(self.column)) + ' ' + + style.SQL_KEYWORD(index_opts) + ' );') return sql def post_create_sql(self, style, db_table): @@ -62,17 +62,17 @@ class PostGISField(Field): post_sql = self._add_geom(style, db_table) # If the user wants to index this data, then get the indexing SQL as well. - if self._index: + if self.spatial_index: return (post_sql, self._geom_index(style, db_table)) else: return (post_sql,) def _post_delete_sql(self, style, db_table): "Drops the geometry column." - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ - style.SQL_TABLE(gqn(db_table)) + ', ' + \ - style.SQL_FIELD(gqn(self.column)) + ');' + sql = (style.SQL_KEYWORD('SELECT ') + + style.SQL_KEYWORD('DropGeometryColumn') + '(' + + style.SQL_TABLE(gqn(db_table)) + ', ' + + style.SQL_FIELD(gqn(self.column)) + ');') return sql def db_type(self): @@ -88,8 +88,8 @@ class PostGISField(Field): SRID of the field. Specifically, this routine will substitute in the ST_Transform() function call. """ - if value is None or value.srid == self._srid: + if value is None or value.srid == self.srid: return '%s' else: # Adding Transform() to the SQL placeholder. - return '%s(%%s, %s)' % (TRANSFORM, self._srid) + return '%s(%%s, %s)' % (TRANSFORM, self.srid) diff --git a/django/contrib/gis/db/backend/postgis/models.py b/django/contrib/gis/db/backend/postgis/models.py index 4a814dc1fe2..de9a1ee1c60 100644 --- a/django/contrib/gis/db/backend/postgis/models.py +++ b/django/contrib/gis/db/backend/postgis/models.py @@ -2,12 +2,6 @@ The GeometryColumns and SpatialRefSys models for the PostGIS backend. """ from django.db import models -from django.contrib.gis.models import SpatialRefSysMixin - -# Checking for the presence of GDAL (needed for the SpatialReference object) -from django.contrib.gis.gdal import HAS_GDAL -if HAS_GDAL: - from django.contrib.gis.gdal import SpatialReference class GeometryColumns(models.Model): """ @@ -28,7 +22,7 @@ class GeometryColumns(models.Model): @classmethod def table_name_col(cls): """ - Returns the name of the metadata column used to store the + Returns the name of the metadata column used to store the the feature table name. """ return 'f_table_name' @@ -36,7 +30,7 @@ class GeometryColumns(models.Model): @classmethod def geom_col_name(cls): """ - Returns the name of the metadata column used to store the + Returns the name of the metadata column used to store the the feature geometry column. """ return 'f_geometry_column' @@ -46,7 +40,7 @@ class GeometryColumns(models.Model): (self.f_table_name, self.f_geometry_column, self.coord_dimension, self.type, self.srid) -class SpatialRefSys(models.Model, SpatialRefSysMixin): +class SpatialRefSys(models.Model): """ The 'spatial_ref_sys' table from PostGIS. See the PostGIS documentaiton at Ch. 4.2.1. @@ -58,6 +52,7 @@ class SpatialRefSys(models.Model, SpatialRefSysMixin): proj4text = models.CharField(max_length=2048) class Meta: + abstract = True db_table = 'spatial_ref_sys' @property diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 87807804021..37671c50a28 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -42,6 +42,7 @@ if MAJOR_VERSION >= 1: ASGML = get_func('AsGML') ASSVG = get_func('AsSVG') CENTROID = get_func('Centroid') + COLLECT = get_func('Collect') DIFFERENCE = get_func('Difference') DISTANCE = get_func('Distance') DISTANCE_SPHERE = get_func('distance_sphere') diff --git a/django/contrib/gis/db/backend/util.py b/django/contrib/gis/db/backend/util.py index a19dd975c1d..397ebceadcd 100644 --- a/django/contrib/gis/db/backend/util.py +++ b/django/contrib/gis/db/backend/util.py @@ -1,4 +1,21 @@ -from types import UnicodeType +""" +A collection of utility routines and classes used by the spatial +backends. +""" + +def getstatusoutput(cmd): + """ + Executes a shell command on the platform using subprocess.Popen and + return a tuple of the status and stdout output. + """ + from subprocess import Popen, PIPE + # Set stdout and stderr to PIPE because we want to capture stdout and + # prevent stderr from displaying. + p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + # We use p.communicate() instead of p.wait() to avoid deadlocks if the + # output buffers exceed POSIX buffer size. + stdout, stderr = p.communicate() + return p.returncode, stdout.strip() def gqn(val): """ @@ -7,7 +24,7 @@ def gqn(val): backend quotename function). """ if isinstance(val, basestring): - if isinstance(val, UnicodeType): val = val.encode('ascii') + if isinstance(val, unicode): val = val.encode('ascii') return "'%s'" % val else: return str(val) diff --git a/django/contrib/gis/db/models/__init__.py b/django/contrib/gis/db/models/__init__.py index 270c73b2835..47a326446b3 100644 --- a/django/contrib/gis/db/models/__init__.py +++ b/django/contrib/gis/db/models/__init__.py @@ -7,9 +7,6 @@ from django.contrib.gis.db.models.aggregates import * # The GeoManager from django.contrib.gis.db.models.manager import GeoManager -# The GeoQ object -from django.contrib.gis.db.models.query import GeoQ - # The geographic-enabled fields. from django.contrib.gis.db.models.fields import \ GeometryField, PointField, LineStringField, PolygonField, \ diff --git a/django/contrib/gis/db/models/aggregates.py b/django/contrib/gis/db/models/aggregates.py index ab90d4d6788..7c8ab694c4f 100644 --- a/django/contrib/gis/db/models/aggregates.py +++ b/django/contrib/gis/db/models/aggregates.py @@ -5,7 +5,7 @@ from django.contrib.gis.db.models.sql import GeomField class GeoAggregate(Aggregate): def add_to_query(self, query, alias, col, source, is_summary): - if hasattr(source, '_geom'): + if hasattr(source, 'geom_type'): # Doing additional setup on the Query object for spatial aggregates. aggregate = getattr(query.aggregates_module, self.name) @@ -18,6 +18,9 @@ class GeoAggregate(Aggregate): super(GeoAggregate, self).add_to_query(query, alias, col, source, is_summary) +class Collect(GeoAggregate): + name = 'Collect' + class Extent(GeoAggregate): name = 'Extent' diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index f21cab13ba1..278e9896eea 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -8,12 +8,16 @@ from django.contrib.gis.measure import Distance # reference system table w/o using the ORM. from django.contrib.gis.models import get_srid_info -#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. +def deprecated_property(func): + from warnings import warn + warn('This attribute has been deprecated, pleas use "%s" instead.' % func.__name__[1:]) + return property(func) + class GeometryField(SpatialBackend.Field): "The base GIS field -- maps to the OpenGIS Specification Geometry type." # The OpenGIS Geometry name. - _geom = 'GEOMETRY' + geom_type = 'GEOMETRY' # Geodetic units. geodetic_units = ('Decimal Degree', 'degree') @@ -37,15 +41,15 @@ class GeometryField(SpatialBackend.Field): """ # Setting the index flag with the value of the `spatial_index` keyword. - self._index = spatial_index + self.spatial_index = spatial_index # Setting the SRID and getting the units. Unit information must be # easily available in the field instance for distance queries. - self._srid = srid - self._unit, self._unit_name, self._spheroid = get_srid_info(srid) + self.srid = srid + self.units, self.units_name, self._spheroid = get_srid_info(srid) # Setting the dimension of the geometry field. - self._dim = dim + self.dim = dim # Setting the verbose_name keyword argument with the positional # first parameter, so this works like normal fields. @@ -53,6 +57,29 @@ class GeometryField(SpatialBackend.Field): super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function + # The following properties are for formerly private variables that are now + # public for GeometryField. Because of their use by third-party applications, + # a deprecation warning is issued to notify them to use new attribute name. + def _deprecated_warning(self, old_name, new_name): + from warnings import warn + warn('The `%s` attribute name is deprecated, please update your code to use `%s` instead.' % + (old_name, new_name)) + + @property + def _geom(self): + self._deprecated_warning('_geom', 'geom_type') + return self.geom_type + + @property + def _index(self): + self._deprecated_warning('_index', 'spatial_index') + return self.spatial_index + + @property + def _srid(self): + self._deprecated_warning('_srid', 'srid') + return self.srid + ### Routines specific to GeometryField ### @property def geodetic(self): @@ -60,7 +87,7 @@ class GeometryField(SpatialBackend.Field): Returns true if this field's SRID corresponds with a coordinate system that uses non-projected units (e.g., latitude/longitude). """ - return self._unit_name in self.geodetic_units + return self.units_name in self.geodetic_units def get_distance(self, dist_val, lookup_type): """ @@ -80,7 +107,7 @@ class GeometryField(SpatialBackend.Field): # Spherical distance calculation parameter should be in meters. dist_param = dist.m else: - dist_param = getattr(dist, Distance.unit_attname(self._unit_name)) + dist_param = getattr(dist, Distance.unit_attname(self.units_name)) else: # Assuming the distance is in the units of the field. dist_param = dist @@ -127,8 +154,8 @@ class GeometryField(SpatialBackend.Field): has no SRID, then that of the field will be returned. """ gsrid = geom.srid # SRID of given geometry. - if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1): - return self._srid + if gsrid is None or self.srid == -1 or (gsrid == -1 and self.srid != -1): + return self.srid else: return gsrid @@ -141,8 +168,9 @@ class GeometryField(SpatialBackend.Field): def formfield(self, **kwargs): defaults = {'form_class' : forms.GeometryField, - 'geom_type' : self._geom, 'null' : self.null, + 'geom_type' : self.geom_type, + 'srid' : self.srid, } defaults.update(kwargs) return super(GeometryField, self).formfield(**defaults) @@ -190,22 +218,22 @@ class GeometryField(SpatialBackend.Field): # The OpenGIS Geometry Type Fields class PointField(GeometryField): - _geom = 'POINT' + geom_type = 'POINT' class LineStringField(GeometryField): - _geom = 'LINESTRING' + geom_type = 'LINESTRING' class PolygonField(GeometryField): - _geom = 'POLYGON' + geom_type = 'POLYGON' class MultiPointField(GeometryField): - _geom = 'MULTIPOINT' + geom_type = 'MULTIPOINT' class MultiLineStringField(GeometryField): - _geom = 'MULTILINESTRING' + geom_type = 'MULTILINESTRING' class MultiPolygonField(GeometryField): - _geom = 'MULTIPOLYGON' + geom_type = 'MULTIPOLYGON' class GeometryCollectionField(GeometryField): - _geom = 'GEOMETRYCOLLECTION' + geom_type = 'GEOMETRYCOLLECTION' diff --git a/django/contrib/gis/db/models/mixin.py b/django/contrib/gis/db/models/mixin.py deleted file mode 100644 index 475a053b8f9..00000000000 --- a/django/contrib/gis/db/models/mixin.py +++ /dev/null @@ -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 diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py index 34276a6d63a..82158486b30 100644 --- a/django/contrib/gis/db/models/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -44,13 +44,13 @@ class GeometryProxy(object): be used to set the geometry as well. """ # The OGC Geometry type of the field. - gtype = self._field._geom + gtype = self._field.geom_type # The geometry type must match that of the field -- unless the # general GeometryField is used. if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): # Assigning the SRID to the geometry. - if value.srid is None: value.srid = self._field._srid + if value.srid is None: value.srid = self._field.srid elif isinstance(value, (NoneType, StringType, UnicodeType)): # Set with None, WKT, or HEX pass diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index fa127b5aafd..bdb6a670dcd 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -9,10 +9,6 @@ from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField from django.contrib.gis.measure import Area, Distance from django.contrib.gis.models import get_srid_info -# For backwards-compatibility; Q object should work just fine -# after queryset-refactor. -class GeoQ(Q): pass - class GeomSQL(object): "Simple wrapper object for geometric SQL." def __init__(self, geo_sql): @@ -44,10 +40,10 @@ class GeoQuerySet(QuerySet): s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' s['procedure_args']['tolerance'] = tolerance s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. - elif SpatialBackend.postgis: + elif SpatialBackend.postgis or SpatialBackend.spatialite: if not geo_field.geodetic: # Getting the area units of the geographic field. - s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name)) + s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name)) else: # TODO: Do we want to support raw number areas for geodetic fields? raise Exception('Area on geodetic coordinate systems not supported.') @@ -196,10 +192,18 @@ class GeoQuerySet(QuerySet): Scales the geometry to a new size by multiplying the ordinates with the given x,y,z scale factors. """ - s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', - 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, - 'select_field' : GeomField(), - } + if SpatialBackend.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D scaling.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } return self._spatial_attribute('scale', s, **kwargs) def svg(self, **kwargs): @@ -226,10 +230,18 @@ class GeoQuerySet(QuerySet): Translates the geometry to a new location using the given numeric parameters as offsets. """ - s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', - 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, - 'select_field' : GeomField(), - } + if SpatialBackend.spatialite: + if z != 0.0: + raise NotImplementedError('SpatiaLite does not support 3D translation.') + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', + 'procedure_args' : {'x' : x, 'y' : y}, + 'select_field' : GeomField(), + } + else: + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } return self._spatial_attribute('translate', s, **kwargs) def transform(self, srid=4326, **kwargs): @@ -415,7 +427,7 @@ class GeoQuerySet(QuerySet): if geo_field.geodetic: dist_att = 'm' else: - dist_att = Distance.unit_attname(geo_field._unit_name) + dist_att = Distance.unit_attname(geo_field.units_name) # Shortcut booleans for what distance function we're using. distance = func == 'distance' @@ -430,7 +442,7 @@ class GeoQuerySet(QuerySet): lookup_params = [geom or 'POINT (0 0)', 0] # If the spheroid calculation is desired, either by the `spheroid` - # keyword or wehn calculating the length of geodetic field, make + # keyword or when calculating the length of geodetic field, make # sure the 'spheroid' distance setting string is passed in so we # get the correct spatial stored procedure. if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): @@ -456,6 +468,9 @@ class GeoQuerySet(QuerySet): else: geodetic = geo_field.geodetic + if SpatialBackend.spatialite and geodetic: + raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.') + if distance: if self.query.transformed_srid: # Setting the `geom_args` flag to false because we want to handle @@ -467,12 +482,22 @@ class GeoQuerySet(QuerySet): if geom.srid is None or geom.srid == self.query.transformed_srid: # If the geom parameter srid is None, it is assumed the coordinates # are in the transformed units. A placeholder is used for the - # geometry parameter. - procedure_fmt += ', %%s' + # geometry parameter. `GeomFromText` constructor is also needed + # to wrap geom placeholder for SpatiaLite. + if SpatialBackend.spatialite: + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.from_text, self.query.transformed_srid) + else: + procedure_fmt += ', %%s' else: # We need to transform the geom to the srid specified in `transform()`, # so wrapping the geometry placeholder in transformation SQL. - procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + # SpatiaLite also needs geometry placeholder wrapped in `GeomFromText` + # constructor. + if SpatialBackend.spatialite: + procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (SpatialBackend.transform, SpatialBackend.from_text, + geom.srid, self.query.transformed_srid) + else: + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) else: # `transform()` was not used on this GeoQuerySet. procedure_fmt = '%(geo_col)s,%(geom)s' @@ -483,9 +508,9 @@ class GeoQuerySet(QuerySet): # procedures may only do queries from point columns to point geometries # some error checking is required. if not isinstance(geo_field, PointField): - raise TypeError('Spherical distance calculation only supported on PointFields.') + raise ValueError('Spherical distance calculation only supported on PointFields.') if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': - raise TypeError('Spherical distance calculation only supported with Point Geometry parameters') + raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') # The `function` procedure argument needs to be set differently for # geodetic distance calculations. if spheroid: diff --git a/django/contrib/gis/db/models/sql/aggregates.py b/django/contrib/gis/db/models/sql/aggregates.py index d7f4acfcf14..370182c721b 100644 --- a/django/contrib/gis/db/models/sql/aggregates.py +++ b/django/contrib/gis/db/models/sql/aggregates.py @@ -44,8 +44,17 @@ elif SpatialBackend.oracle: return None def convert_geom(clob, geo_field): - if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) - else: return None + if clob: + return SpatialBackend.Geometry(clob.read(), geo_field.srid) + else: + return None +elif SpatialBackend.spatialite: + # SpatiaLite returns WKT. + def convert_geom(wkt, geo_field): + if wkt: + return SpatialBackend.Geometry(wkt, geo_field.srid) + else: + return None class GeoAggregate(Aggregate): # Overriding the SQL template with the geographic one. @@ -71,6 +80,10 @@ class GeoAggregate(Aggregate): if not self.sql_function: raise NotImplementedError('This aggregate functionality not implemented for your spatial backend.') +class Collect(GeoAggregate): + conversion_class = GeomField + sql_function = SpatialBackend.collect + class Extent(GeoAggregate): is_extent = True sql_function = SpatialBackend.extent diff --git a/django/contrib/gis/db/models/sql/conversion.py b/django/contrib/gis/db/models/sql/conversion.py index e7dd2fb62f9..cfca640bfad 100644 --- a/django/contrib/gis/db/models/sql/conversion.py +++ b/django/contrib/gis/db/models/sql/conversion.py @@ -2,11 +2,15 @@ This module holds simple classes used by GeoQuery.convert_values to convert geospatial values from the database. """ +from django.contrib.gis.db.backend import SpatialBackend + class BaseField(object): def get_internal_type(self): "Overloaded method so OracleQuery.convert_values doesn't balk." return None +if SpatialBackend.oracle: BaseField.empty_strings_allowed = False + class AreaField(BaseField): "Wrapper for Area values." def __init__(self, area_att): diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 712316e4341..e09cb7ce3cb 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -208,13 +208,14 @@ class GeoQuery(sql.Query): if SpatialBackend.oracle: # Running through Oracle's first. value = super(GeoQuery, self).convert_values(value, field or GeomField()) + if isinstance(field, DistanceField): # Using the field's distance attribute, can instantiate # `Distance` with the right context. value = Distance(**{field.distance_att : value}) elif isinstance(field, AreaField): value = Area(**{field.area_att : value}) - elif isinstance(field, GeomField): + elif isinstance(field, GeomField) and value: value = SpatialBackend.Geometry(value) return value @@ -260,7 +261,7 @@ class GeoQuery(sql.Query): selection formats in order to retrieve geometries in OGC WKT. For all other fields a simple '%s' format string is returned. """ - if SpatialBackend.select and hasattr(fld, '_geom'): + if SpatialBackend.select and hasattr(fld, 'geom_type'): # This allows operations to be done on fields in the SELECT, # overriding their values -- used by the Oracle and MySQL # spatial backends to get database values as WKT, and by the @@ -270,8 +271,10 @@ class GeoQuery(sql.Query): # Because WKT doesn't contain spatial reference information, # the SRID is prefixed to the returned WKT to ensure that the # transformed geometries have an SRID different than that of the - # field -- this is only used by `transform` for Oracle backends. - if self.transformed_srid and SpatialBackend.oracle: + # field -- this is only used by `transform` for Oracle and + # SpatiaLite backends. + if self.transformed_srid and ( SpatialBackend.oracle or + SpatialBackend.spatialite ): sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt) else: sel_fmt = '%s' diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index 52cf5cd9b48..61c01d29021 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -15,7 +15,7 @@ class GeoAnnotation(object): """ def __init__(self, field, value, where): self.geodetic = field.geodetic - self.geom_type = field._geom + self.geom_type = field.geom_type self.value = value self.where = tuple(where) @@ -37,7 +37,7 @@ class GeoWhereNode(WhereNode): obj, lookup_type, value = data alias, col, field = obj.alias, obj.col, obj.field - if not hasattr(field, "_geom"): + if not hasattr(field, "geom_type"): # Not a geographic field, so call `WhereNode.add`. return super(GeoWhereNode, self).add(data, connector) else: @@ -50,7 +50,7 @@ class GeoWhereNode(WhereNode): # Get the SRID of the geometry field that the expression was meant # to operate on -- it's needed to determine whether transformation # SQL is necessary. - srid = geo_fld._srid + srid = geo_fld.srid # Getting the quoted representation of the geometry column that # the expression is operating on. @@ -58,8 +58,8 @@ class GeoWhereNode(WhereNode): # If it's in a different SRID, we'll need to wrap in # transformation SQL. - if not srid is None and srid != field._srid and SpatialBackend.transform: - placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field._srid) + if not srid is None and srid != field.srid and SpatialBackend.transform: + placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field.srid) else: placeholder = '%s' diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index f60c722f765..3e8b271f4be 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -19,6 +19,7 @@ class GeometryField(forms.Field): def __init__(self, **kwargs): self.null = kwargs.pop('null') self.geom_type = kwargs.pop('geom_type') + self.srid = kwargs.pop('srid') super(GeometryField, self).__init__(**kwargs) def clean(self, value): diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py index 4453dd89fca..c27d60311e5 100644 --- a/django/contrib/gis/models.py +++ b/django/contrib/gis/models.py @@ -15,24 +15,24 @@ class SpatialRefSysMixin(object): The SpatialRefSysMixin is a class used by the database-dependent SpatialRefSys objects to reduce redundnant code. """ - # For pulling out the spheroid from the spatial reference string. This # regular expression is used only if the user does not have GDAL installed. - # TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b' - # parameter. + # TODO: Flattening not used in all ellipsoids, could also be a minor axis, + # or 'b' parameter. spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P.+)\",(?P\d+(\.\d+)?),(?P\d{3}\.\d+),') # For pulling out the units on platforms w/o GDAL installed. # TODO: Figure out how to pull out angular units of projected coordinate system and - # fix for LOCAL_CS types. GDAL should be highly recommended for performing + # fix for LOCAL_CS types. GDAL should be highly recommended for performing # distance queries. units_regex = re.compile(r'.+UNIT ?\["(?P[\w \'\(\)]+)", ?(?P[\d\.]+)(,AUTHORITY\["(?P[\w \'\(\)]+)","(?P\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P[\w \'\(\)]+)","(?P\d+)"\])?\]$') - + def srs(self): """ Returns a GDAL SpatialReference object, if GDAL is installed. """ if HAS_GDAL: + # TODO: Is caching really necessary here? Is complexity worth it? if hasattr(self, '_srs'): # Returning a clone of the cached SpatialReference object. return self._srs.clone() @@ -45,7 +45,13 @@ class SpatialRefSysMixin(object): return self.srs except Exception, msg: pass - + + try: + self._srs = SpatialReference(self.proj4text) + return self.srs + except Exception, msg: + pass + raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) else: raise Exception('GDAL is not installed.') @@ -107,7 +113,7 @@ class SpatialRefSysMixin(object): "Returns the linear units name." if HAS_GDAL: return self.srs.linear_name - elif self.geographic: + elif self.geographic: return None else: m = self.units_regex.match(self.wkt) @@ -181,13 +187,13 @@ class SpatialRefSysMixin(object): sphere_name = srs['spheroid'] else: m = cls.spheroid_regex.match(wkt) - if m: + if m: sphere_params = (float(m.group('major')), float(m.group('flattening'))) sphere_name = m.group('name') - else: + else: return None - - if not string: + + if not string: return sphere_name, sphere_params else: # `string` parameter used to place in format acceptable by PostGIS @@ -195,7 +201,7 @@ class SpatialRefSysMixin(object): radius, flattening = sphere_params[0], sphere_params[2] else: radius, flattening = sphere_params - return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening) + return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening) get_spheroid = classmethod(get_spheroid) def __unicode__(self): @@ -208,76 +214,84 @@ class SpatialRefSysMixin(object): except: return unicode(self.wkt) -# The SpatialRefSys and GeometryColumns models -_srid_info = True -if not PYTHON23 and settings.DATABASE_ENGINE == 'postgresql_psycopg2': - # Because the PostGIS version is checked when initializing the spatial - # backend a `ProgrammingError` will be raised if the PostGIS tables - # and functions are not installed. We catch here so it won't be raised when - # running the Django test suite. `ImportError` is also possible if no ctypes. +# Defining dummy default first; if spatial db, will overrride. +def get_srid_info(srid): + """ + Dummy routine for the backends that do not have the OGC required + spatial metadata tables (like MySQL). + """ + return None, None, None + +# Django test suite on 2.3 platforms will choke on code inside this +# conditional. +if not PYTHON23: try: - from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys + # try/except'ing the importation of SpatialBackend. Have to fail + # silently because this module may be inadvertently invoked by + # non-GeoDjango users (e.g., when the Django test suite executes + # the models.py of all contrib apps). + from django.contrib.gis.db.backend import SpatialBackend + if SpatialBackend.mysql: raise Exception + + # Exposing the SpatialRefSys and GeometryColumns models. + class SpatialRefSys(SpatialBackend.SpatialRefSys, SpatialRefSysMixin): + pass + GeometryColumns = SpatialBackend.GeometryColumns + + # Override `get_srid_info` with something real thing. + def get_srid_info(srid): + """ + Returns the units, unit name, and spheroid WKT associated with the + given SRID from the `spatial_ref_sys` (or equivalent) spatial database + table. We use a database cursor to execute the query because this + function is used when it is not possible to use the ORM (for example, + during field initialization). + """ + # SRID=-1 is a common convention for indicating the geometry has no + # spatial reference information associated with it. Thus, we will + # return all None values without raising an exception. + if srid == -1: return None, None, None + + # Getting the spatial reference WKT associated with the SRID from the + # `spatial_ref_sys` (or equivalent) spatial database table. This query + # cannot be executed using the ORM because this information is needed + # when the ORM cannot be used (e.g., during the initialization of + # `GeometryField`). + from django.db import connection + cur = connection.cursor() + qn = connection.ops.quote_name + stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)' + params = {'table' : qn(SpatialRefSys._meta.db_table), + 'srid_col' : qn('srid'), + 'srid' : srid, + } + if SpatialBackend.spatialite: + if not HAS_GDAL: raise Exception('GDAL is required to use the SpatiaLite backend.') + params['wkt_col'] = 'proj4text' + else: + params['wkt_col'] = qn(SpatialRefSys.wkt_col()) + + # Executing the SQL statement. + cur.execute(stmt % params) + + # Fetching the WKT from the cursor; if the query failed raise an Exception. + fetched = cur.fetchone() + if not fetched: + raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' % + (SpatialRefSys._meta.db_table, srid)) + + if SpatialBackend.spatialite: + # Because the `spatial_ref_sys` table does _not_ contain a WKT column, + # we have to use GDAL to determine the units from the PROJ.4 string. + srs_wkt = SpatialReference(fetched[0]).wkt + else: + srs_wkt = fetched[0] + + # Getting metadata associated with the spatial reference system identifier. + # Specifically, getting the unit information and spheroid information + # (both required for distance queries). + unit, unit_name = SpatialRefSys.get_units(srs_wkt) + spheroid = SpatialRefSys.get_spheroid(srs_wkt) + return unit, unit_name, spheroid except: - _srid_info = False -elif not PYTHON23 and settings.DATABASE_ENGINE == 'oracle': - # Same thing as above, except the GEOS library is attempted to be loaded for - # `BaseSpatialBackend`, and an exception will be raised during the - # Django test suite if it doesn't exist. - try: - from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys - except: - _srid_info = False -else: - _srid_info = False - -if _srid_info: - def get_srid_info(srid): - """ - Returns the units, unit name, and spheroid WKT associated with the - given SRID from the `spatial_ref_sys` (or equivalent) spatial database - table. We use a database cursor to execute the query because this - function is used when it is not possible to use the ORM (for example, - during field initialization). - """ - # SRID=-1 is a common convention for indicating the geometry has no - # spatial reference information associated with it. Thus, we will - # return all None values without raising an exception. - if srid == -1: return None, None, None - - # Getting the spatial reference WKT associated with the SRID from the - # `spatial_ref_sys` (or equivalent) spatial database table. This query - # cannot be executed using the ORM because this information is needed - # when the ORM cannot be used (e.g., during the initialization of - # `GeometryField`). - from django.db import connection - cur = connection.cursor() - qn = connection.ops.quote_name - stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)' - stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table), - 'wkt_col' : qn(SpatialRefSys.wkt_col()), - 'srid_col' : qn('srid'), - 'srid' : srid, - } - cur.execute(stmt) - - # Fetching the WKT from the cursor; if the query failed raise an Exception. - fetched = cur.fetchone() - if not fetched: - raise ValueError('Failed to find spatial reference entry in "%s" corresponding to SRID=%s.' % - (SpatialRefSys._meta.db_table, srid)) - srs_wkt = fetched[0] - - # Getting metadata associated with the spatial reference system identifier. - # Specifically, getting the unit information and spheroid information - # (both required for distance queries). - unit, unit_name = SpatialRefSys.get_units(srs_wkt) - spheroid = SpatialRefSys.get_spheroid(srs_wkt) - return unit, unit_name, spheroid -else: - def get_srid_info(srid): - """ - Dummy routine for the backends that do not have the OGC required - spatial metadata tables (like MySQL). - """ - return None, None, None - + pass diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 7a3ac25aa15..e27cf6e3c50 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -9,28 +9,26 @@ def geo_suite(): some backends). """ from django.conf import settings - from django.contrib.gis.tests.utils import mysql, oracle, postgis - from django.contrib.gis import gdal, utils + from django.contrib.gis.gdal import HAS_GDAL + from django.contrib.gis.utils import HAS_GEOIP + from django.contrib.gis.tests.utils import mysql # The test suite. s = unittest.TestSuite() - # Adding the GEOS tests. (__future__) - from django.contrib.gis.geos import tests as geos_tests - s.addTest(geos_tests.suite()) - - # Test apps that require use of a spatial database (e.g., creation of models) + # Tests that require use of a spatial database (e.g., creation of models) test_apps = ['geoapp', 'relatedapp'] - if oracle or postgis: - test_apps.append('distapp') - # Tests that do not require setting up and tearing down a spatial database - # and are modules in `django.contrib.gis.tests`. + # Tests that do not require setting up and tearing down a spatial database. test_suite_names = [ 'test_measure', ] - if gdal.HAS_GDAL: + # Tests applications that require a test spatial db. + if not mysql: + test_apps.append('distapp') + + if HAS_GDAL: # These tests require GDAL. test_suite_names.append('test_spatialrefsys') test_apps.append('layermap') @@ -39,14 +37,25 @@ def geo_suite(): from django.contrib.gis.gdal import tests as gdal_tests s.addTest(gdal_tests.suite()) else: - print >>sys.stderr, "GDAL not available - no GDAL tests will be run." + print >>sys.stderr, "GDAL not available - no tests requiring GDAL will be run." - if utils.HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): + if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): test_suite_names.append('test_geoip') + # Adding the rest of the suites from the modules specified + # in the `test_suite_names`. for suite_name in test_suite_names: tsuite = import_module('django.contrib.gis.tests.' + suite_name) s.addTest(tsuite.suite()) + + # Adding the GEOS tests _last_. Doing this because if suite starts + # immediately with this test while after running syncdb, it will cause a + # segmentation fault. My initial guess is that SpatiaLite is still in + # critical areas of non thread-safe GEOS code when the test suite is run. + # TODO: Confirm my reasoning. Are there other consequences? + from django.contrib.gis.geos import tests as geos_tests + s.addTest(geos_tests.suite()) + return s, test_apps def run_gis_tests(test_labels, **kwargs): @@ -84,8 +93,8 @@ def run_gis_tests(test_labels, **kwargs): # Creating the test suite, adding the test models to INSTALLED_APPS, and # adding the model test suites to our suite package. gis_suite, test_apps = geo_suite() - for test_app in test_apps: - module_name = 'django.contrib.gis.tests.%s' % test_app + for test_model in test_apps: + module_name = 'django.contrib.gis.tests.%s' % test_model if mysql: test_module = 'tests_mysql' else: @@ -114,12 +123,12 @@ def run_gis_tests(test_labels, **kwargs): def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=None): """ - This module allows users to run tests for GIS apps that require the creation + This module allows users to run tests for GIS apps that require the creation of a spatial database. Currently, this is only required for PostgreSQL as PostGIS needs extra overhead in test database creation. - In order to create a PostGIS database, the DATABASE_USER (or - TEST_DATABASE_USER, if defined) will require superuser priviliges. + In order to create a PostGIS database, the DATABASE_USER (or + TEST_DATABASE_USER, if defined) will require superuser priviliges. To accomplish this outside the `postgres` user, you have a few options: (A) Make your user a super user: @@ -133,11 +142,11 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite= (B) Create your own PostgreSQL database as a local user: 1. Initialize database: `initdb -D /path/to/user/db` 2. If there's already a Postgres instance on the machine, it will need - to use a different TCP port than 5432. Edit postgresql.conf (in - /path/to/user/db) to change the database port (e.g. `port = 5433`). + to use a different TCP port than 5432. Edit postgresql.conf (in + /path/to/user/db) to change the database port (e.g. `port = 5433`). 3. Start this database `pg_ctl -D /path/to/user/db start` - (C) On Windows platforms the pgAdmin III utility may also be used as + (C) On Windows platforms the pgAdmin III utility may also be used as a simple way to add superuser privileges to your database user. The TEST_RUNNER needs to be set in your settings like so: @@ -145,10 +154,10 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite= TEST_RUNNER='django.contrib.gis.tests.run_tests' Note: This test runner assumes that the PostGIS SQL files ('lwpostgis.sql' - and 'spatial_ref_sys.sql') are installed in the directory specified by + and 'spatial_ref_sys.sql') are installed in the directory specified by `pg_config --sharedir` (and defaults to /usr/local/share if that fails). This behavior is overridden if POSTGIS_SQL_PATH is set in your settings. - + Windows users should set POSTGIS_SQL_PATH manually because the output of `pg_config` uses paths like 'C:/PROGRA~1/POSTGR~1/..'. @@ -160,18 +169,21 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite= from django.test.simple import build_suite, build_test from django.test.utils import setup_test_environment, teardown_test_environment - # The `create_spatial_db` routine abstracts away all the steps needed + # The `create_test_spatial_db` routine abstracts away all the steps needed # to properly construct a spatial database for the backend. - from django.contrib.gis.db.backend import create_spatial_db + from django.contrib.gis.db.backend import create_test_spatial_db # Setting up for testing. setup_test_environment() settings.DEBUG = False old_name = settings.DATABASE_NAME + # Creating the test spatial database. + create_test_spatial_db(verbosity=verbosity) + # The suite may be passed in manually, e.g., when we run the GeoDjango test, - # we want to build it and pass it in due to some customizations. Otherwise, - # the normal test suite creation process from `django.test.simple.run_tests` + # we want to build it and pass it in due to some customizations. Otherwise, + # the normal test suite creation process from `django.test.simple.run_tests` # is used to create the test suite. if suite is None: suite = unittest.TestSuite() @@ -185,13 +197,10 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite= else: for app in get_apps(): suite.addTest(build_suite(app)) - + for test in extra_tests: suite.addTest(test) - # Creating the test spatial database. - create_spatial_db(test=True, verbosity=verbosity) - # Executing the tests (including the model tests), and destorying the # test database after the tests have completed. result = unittest.TextTestRunner(verbosity=verbosity).run(suite) diff --git a/django/contrib/gis/tests/layermap/cities/cities.dbf b/django/contrib/gis/tests/data/cities/cities.dbf similarity index 100% rename from django/contrib/gis/tests/layermap/cities/cities.dbf rename to django/contrib/gis/tests/data/cities/cities.dbf diff --git a/django/contrib/gis/tests/layermap/cities/cities.prj b/django/contrib/gis/tests/data/cities/cities.prj similarity index 100% rename from django/contrib/gis/tests/layermap/cities/cities.prj rename to django/contrib/gis/tests/data/cities/cities.prj diff --git a/django/contrib/gis/tests/layermap/cities/cities.shp b/django/contrib/gis/tests/data/cities/cities.shp similarity index 100% rename from django/contrib/gis/tests/layermap/cities/cities.shp rename to django/contrib/gis/tests/data/cities/cities.shp diff --git a/django/contrib/gis/tests/layermap/cities/cities.shx b/django/contrib/gis/tests/data/cities/cities.shx similarity index 100% rename from django/contrib/gis/tests/layermap/cities/cities.shx rename to django/contrib/gis/tests/data/cities/cities.shx diff --git a/django/contrib/gis/tests/layermap/counties/counties.dbf b/django/contrib/gis/tests/data/counties/counties.dbf similarity index 100% rename from django/contrib/gis/tests/layermap/counties/counties.dbf rename to django/contrib/gis/tests/data/counties/counties.dbf diff --git a/django/contrib/gis/tests/layermap/counties/counties.shp b/django/contrib/gis/tests/data/counties/counties.shp similarity index 100% rename from django/contrib/gis/tests/layermap/counties/counties.shp rename to django/contrib/gis/tests/data/counties/counties.shp diff --git a/django/contrib/gis/tests/layermap/counties/counties.shx b/django/contrib/gis/tests/data/counties/counties.shx similarity index 100% rename from django/contrib/gis/tests/layermap/counties/counties.shx rename to django/contrib/gis/tests/data/counties/counties.shx diff --git a/django/contrib/gis/tests/layermap/interstates/interstates.dbf b/django/contrib/gis/tests/data/interstates/interstates.dbf similarity index 100% rename from django/contrib/gis/tests/layermap/interstates/interstates.dbf rename to django/contrib/gis/tests/data/interstates/interstates.dbf diff --git a/django/contrib/gis/tests/layermap/interstates/interstates.prj b/django/contrib/gis/tests/data/interstates/interstates.prj similarity index 100% rename from django/contrib/gis/tests/layermap/interstates/interstates.prj rename to django/contrib/gis/tests/data/interstates/interstates.prj diff --git a/django/contrib/gis/tests/layermap/interstates/interstates.shp b/django/contrib/gis/tests/data/interstates/interstates.shp similarity index 100% rename from django/contrib/gis/tests/layermap/interstates/interstates.shp rename to django/contrib/gis/tests/data/interstates/interstates.shp diff --git a/django/contrib/gis/tests/layermap/interstates/interstates.shx b/django/contrib/gis/tests/data/interstates/interstates.shx similarity index 100% rename from django/contrib/gis/tests/layermap/interstates/interstates.shx rename to django/contrib/gis/tests/data/interstates/interstates.shx diff --git a/django/contrib/gis/tests/distapp/data.py b/django/contrib/gis/tests/distapp/data.py index fefe3139058..77c06d65d86 100644 --- a/django/contrib/gis/tests/distapp/data.py +++ b/django/contrib/gis/tests/distapp/data.py @@ -31,3 +31,6 @@ stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95 interstates = (('I-25', 'LINESTRING(-104.4780170766108 36.66698791870694, -104.4468522338495 36.79925409393386, -104.46212692626 36.9372149776075, -104.5126119783768 37.08163268820887, -104.5247764602161 37.29300499892048, -104.7084397427668 37.49150259925398, -104.8126599016282 37.69514285621863, -104.8452887035466 37.87613395659479, -104.7160169341003 38.05951763337799, -104.6165437927668 38.30432045855106, -104.6437227858174 38.53979986564737, -104.7596170387259 38.7322907594295, -104.8380078676822 38.89998460604341, -104.8501253693506 39.09980189213358, -104.8791648316464 39.24368776457503, -104.8635041274215 39.3785278162751, -104.8894471170052 39.5929228239605, -104.9721242843344 39.69528482419685, -105.0112104500356 39.7273080432394, -105.0010368577104 39.76677607811571, -104.981835619 39.81466504121967, -104.9858891550477 39.88806911250832, -104.9873548059578 39.98117234571016, -104.9766220487419 40.09796423450692, -104.9818565932953 40.36056530662884, -104.9912746373997 40.74904484447656)'), ) + +stx_interstates = (('I-10', 'LINESTRING(924952.5 4220931.6,925065.3 4220931.6,929568.4 4221057.8)'), + ) diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py index f23b277feaf..4030761578f 100644 --- a/django/contrib/gis/tests/distapp/models.py +++ b/django/contrib/gis/tests/distapp/models.py @@ -37,6 +37,13 @@ class SouthTexasZipcode(models.Model): class Interstate(models.Model): "Geodetic model for U.S. Interstates." name = models.CharField(max_length=10) - line = models.LineStringField() + path = models.LineStringField() + objects = models.GeoManager() + def __unicode__(self): return self.name + +class SouthTexasInterstate(models.Model): + "Projected model for South Texas Interstates." + name = models.CharField(max_length=10) + path = models.LineStringField(srid=32140) objects = models.GeoManager() def __unicode__(self): return self.name diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 057e2f715e2..ecc2da2c877 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -5,11 +5,11 @@ from django.db.models import Q from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import GEOSGeometry, Point, LineString from django.contrib.gis.measure import D # alias for Distance -from django.contrib.gis.db.models import GeoQ -from django.contrib.gis.tests.utils import oracle, postgis, no_oracle +from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite -from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode -from data import au_cities, interstates, stx_cities, stx_zips +from models import AustraliaCity, Interstate, SouthTexasInterstate, \ + SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode +from data import au_cities, interstates, stx_interstates, stx_cities, stx_zips class DistanceTest(unittest.TestCase): @@ -32,9 +32,12 @@ class DistanceTest(unittest.TestCase): # Loading up the cities. def load_cities(city_model, data_tup): for name, x, y in data_tup: - c = city_model(name=name, point=Point(x, y, srid=4326)) - c.save() - + city_model(name=name, point=Point(x, y, srid=4326)).save() + + def load_interstates(imodel, data_tup): + for name, wkt in data_tup: + imodel(name=name, path=wkt).save() + load_cities(SouthTexasCity, stx_cities) load_cities(SouthTexasCityFt, stx_cities) load_cities(AustraliaCity, au_cities) @@ -42,7 +45,7 @@ class DistanceTest(unittest.TestCase): self.assertEqual(9, SouthTexasCity.objects.count()) self.assertEqual(9, SouthTexasCityFt.objects.count()) self.assertEqual(11, AustraliaCity.objects.count()) - + # Loading up the South Texas Zip Codes. for name, wkt in stx_zips: poly = GEOSGeometry(wkt, srid=4269) @@ -52,10 +55,13 @@ class DistanceTest(unittest.TestCase): self.assertEqual(4, CensusZipcode.objects.count()) # Loading up the Interstates. - for name, wkt in interstates: - Interstate(name=name, line=GEOSGeometry(wkt, srid=4326)).save() - self.assertEqual(1, Interstate.objects.count()) + load_interstates(Interstate, interstates) + load_interstates(SouthTexasInterstate, stx_interstates) + self.assertEqual(1, Interstate.objects.count()) + self.assertEqual(1, SouthTexasInterstate.objects.count()) + + @no_spatialite def test02_dwithin(self): "Testing the `dwithin` lookup type." # Distances -- all should be equal (except for the @@ -63,7 +69,7 @@ class DistanceTest(unittest.TestCase): # approximate). tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)] au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)] - + # Expected cities for Australia and Texas. tx_cities = ['Downtown Houston', 'Southside Place'] au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong'] @@ -86,27 +92,29 @@ class DistanceTest(unittest.TestCase): if isinstance(dist, tuple): if oracle: dist = dist[1] else: dist = dist[0] - + # Creating the query set. qs = AustraliaCity.objects.order_by('name') if type_error: # A TypeError should be raised on PostGIS when trying to pass - # Distance objects into a DWithin query using a geodetic field. + # Distance objects into a DWithin query using a geodetic field. self.assertRaises(TypeError, AustraliaCity.objects.filter, point__dwithin=(self.au_pnt, dist)) else: self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist)))) - + def test03a_distance_method(self): "Testing the `distance` GeoQuerySet method on projected coordinate systems." # The point for La Grange, TX lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326) - # Reference distances in feet and in meters. Got these values from + # Reference distances in feet and in meters. Got these values from # using the provided raw SQL statements. # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity; m_distances = [147075.069813, 139630.198056, 140888.552826, 138809.684197, 158309.246259, 212183.594374, 70870.188967, 165337.758878, 139196.085105] # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft; + # Oracle 11 thinks this is not a projected coordinate system, so it's s + # not tested. ft_distances = [482528.79154625, 458103.408123001, 462231.860397575, 455411.438904354, 519386.252102563, 696139.009211594, 232513.278304279, 542445.630586414, 456679.155883207] @@ -115,8 +123,12 @@ class DistanceTest(unittest.TestCase): # with different projected coordinate systems. dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point') dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter - dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter. - dist4 = SouthTexasCityFt.objects.distance(lagrange) + if spatialite or oracle: + dist_qs = [dist1, dist2] + else: + dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter. + dist4 = SouthTexasCityFt.objects.distance(lagrange) + dist_qs = [dist1, dist2, dist3, dist4] # Original query done on PostGIS, have to adjust AlmostEqual tolerance # for Oracle. @@ -124,11 +136,12 @@ class DistanceTest(unittest.TestCase): else: tol = 5 # Ensuring expected distances are returned for each distance queryset. - for qs in [dist1, dist2, dist3, dist4]: + for qs in dist_qs: for i, c in enumerate(qs): self.assertAlmostEqual(m_distances[i], c.distance.m, tol) self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol) + @no_spatialite def test03b_distance_method(self): "Testing the `distance` GeoQuerySet method on geodetic coordnate systems." if oracle: tol = 2 @@ -139,8 +152,8 @@ class DistanceTest(unittest.TestCase): if not oracle: # PostGIS is limited to disance queries only to/from point geometries, # ensuring a TypeError is raised if something else is put in. - self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)') - self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1))) + self.assertRaises(ValueError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)') + self.assertRaises(ValueError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1))) # Got the reference distances using the raw SQL statements: # SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11)); @@ -163,8 +176,8 @@ class DistanceTest(unittest.TestCase): "Testing the `distance` GeoQuerySet method used with `transform` on a geographic field." # Normally you can't compute distances from a geometry field # that is not a PointField (on PostGIS). - self.assertRaises(TypeError, CensusZipcode.objects.distance, self.stx_pnt) - + self.assertRaises(ValueError, CensusZipcode.objects.distance, self.stx_pnt) + # We'll be using a Polygon (created by buffering the centroid # of 77005 to 100m) -- which aren't allowed in geographic distance # queries normally, however our field has been transformed to @@ -182,9 +195,11 @@ class DistanceTest(unittest.TestCase): # however. buf1 = z.poly.centroid.buffer(100) buf2 = buf1.transform(4269, clone=True) + ref_zips = ['77002', '77025', '77401'] + for buf in [buf1, buf2]: qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf) - self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) + self.assertEqual(ref_zips, self.get_names(qs)) for i, z in enumerate(qs): self.assertAlmostEqual(z.distance.m, dists_m[i], 5) @@ -194,8 +209,16 @@ class DistanceTest(unittest.TestCase): # (thus, Houston and Southside place will be excluded as tested in # the `test02_dwithin` above). qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20))) - qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20))) - for qs in qs1, qs2: + + # Can't determine the units on SpatiaLite from PROJ.4 string, and + # Oracle 11 incorrectly thinks it is not projected. + if spatialite or oracle: + dist_qs = (qs1,) + else: + qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20))) + dist_qs = (qs1, qs2) + + for qs in dist_qs: cities = self.get_names(qs) self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place']) @@ -206,7 +229,8 @@ class DistanceTest(unittest.TestCase): # If we add a little more distance 77002 should be included. qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300))) self.assertEqual(['77002', '77025', '77401'], self.get_names(qs)) - + + @no_spatialite def test05_geodetic_distance_lookups(self): "Testing distance lookups on geodetic coordinate systems." if not oracle: @@ -216,7 +240,7 @@ class DistanceTest(unittest.TestCase): self.assertRaises(TypeError, AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100)))) # Too many params (4 in this case) should raise a ValueError. - self.assertRaises(ValueError, + self.assertRaises(ValueError, AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')) # Not enough params should raise a ValueError. @@ -235,14 +259,13 @@ class DistanceTest(unittest.TestCase): d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles. # Normal geodetic distance lookup (uses `distance_sphere` on PostGIS. - gq1 = GeoQ(point__distance_lte=(wollongong.point, d1)) - gq2 = GeoQ(point__distance_gte=(wollongong.point, d2)) + gq1 = Q(point__distance_lte=(wollongong.point, d1)) + gq2 = Q(point__distance_gte=(wollongong.point, d2)) qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2) # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid` # instead (we should get the same results b/c accuracy variance won't matter - # in this test case). Using `Q` instead of `GeoQ` to be different (post-qsrf - # it doesn't matter). + # in this test case). if postgis: gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid')) gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid')) @@ -270,12 +293,23 @@ class DistanceTest(unittest.TestCase): "Testing the `length` GeoQuerySet method." # Reference query (should use `length_spheroid`). # SELECT ST_length_spheroid(ST_GeomFromText('', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]'); - len_m = 473504.769553813 - qs = Interstate.objects.length() - if oracle: tol = 2 - else: tol = 5 - self.assertAlmostEqual(len_m, qs[0].length.m, tol) + len_m1 = 473504.769553813 + len_m2 = 4617.668 + if spatialite: + # Does not support geodetic coordinate systems. + self.assertRaises(ValueError, Interstate.objects.length) + else: + qs = Interstate.objects.length() + if oracle: tol = 2 + else: tol = 5 + self.assertAlmostEqual(len_m1, qs[0].length.m, tol) + + # Now doing length on a projected coordinate system. + i10 = SouthTexasInterstate.objects.length().get(name='I-10') + self.assertAlmostEqual(len_m2, i10.length.m, 2) + + @no_spatialite def test08_perimeter(self): "Testing the `perimeter` GeoQuerySet method." # Reference query: diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index 98436731676..c4ce893beea 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -1,5 +1,5 @@ from django.contrib.gis.db import models -from django.contrib.gis.tests.utils import mysql +from django.contrib.gis.tests.utils import mysql, spatialite # MySQL spatial indices can't handle NULL geometries. null_flag = not mysql @@ -12,7 +12,7 @@ class Country(models.Model): class City(models.Model): name = models.CharField(max_length=30) - point = models.PointField() + point = models.PointField() objects = models.GeoManager() def __unicode__(self): return self.name @@ -27,12 +27,13 @@ class State(models.Model): objects = models.GeoManager() def __unicode__(self): return self.name -class Feature(models.Model): - name = models.CharField(max_length=20) - geom = models.GeometryField() - objects = models.GeoManager() - def __unicode__(self): return self.name +if not spatialite: + class Feature(models.Model): + name = models.CharField(max_length=20) + geom = models.GeometryField() + objects = models.GeoManager() + def __unicode__(self): return self.name -class MinusOneSRID(models.Model): - geom = models.PointField(srid=-1) # Minus one SRID. - objects = models.GeoManager() + class MinusOneSRID(models.Model): + geom = models.PointField(srid=-1) # Minus one SRID. + objects = models.GeoManager() diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 2a146d5f599..475a1f7618d 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -1,10 +1,13 @@ import os, unittest -from models import Country, City, PennsylvaniaCity, State, Feature, MinusOneSRID from django.contrib.gis import gdal from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.geos import * from django.contrib.gis.measure import Distance -from django.contrib.gis.tests.utils import no_oracle, no_postgis +from django.contrib.gis.tests.utils import no_oracle, no_postgis, no_spatialite +from models import Country, City, PennsylvaniaCity, State + +if not SpatialBackend.spatialite: + from models import Feature, MinusOneSRID # TODO: Some tests depend on the success/failure of previous tests, these should # be decoupled. This flag is an artifact of this problem, and makes debugging easier; @@ -37,9 +40,11 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(2, Country.objects.count()) self.assertEqual(8, City.objects.count()) - # Oracle cannot handle NULL geometry values w/certain queries. - if SpatialBackend.oracle: n_state = 2 - else: n_state = 3 + # Only PostGIS can handle NULL geometries + if SpatialBackend.postgis or SpatialBackend.spatialite: + n_state = 3 + else: + n_state = 2 self.assertEqual(n_state, State.objects.count()) def test02_proxy(self): @@ -112,6 +117,7 @@ class GeoModelTest(unittest.TestCase): ns.delete() @no_oracle # Oracle does not support KML. + @no_spatialite # SpatiaLite does not support KML. def test03a_kml(self): "Testing KML output from the database using GeoManager.kml()." if DISABLE: return @@ -137,6 +143,7 @@ class GeoModelTest(unittest.TestCase): for ptown in [ptown1, ptown2]: self.assertEqual(ref_kml, ptown.kml) + @no_spatialite # SpatiaLite does not support GML. def test03b_gml(self): "Testing GML output from the database using GeoManager.gml()." if DISABLE: return @@ -150,7 +157,7 @@ class GeoModelTest(unittest.TestCase): if SpatialBackend.oracle: # No precision parameter for Oracle :-/ import re - gml_regex = re.compile(r'-104.60925199\d+,38.25500\d+ ') + gml_regex = re.compile(r'-104.60925\d+,38.25500\d+ ') for ptown in [ptown1, ptown2]: self.assertEqual(True, bool(gml_regex.match(ptown.gml))) else: @@ -180,7 +187,7 @@ class GeoModelTest(unittest.TestCase): self.assertAlmostEqual(ptown.x, p.point.x, prec) self.assertAlmostEqual(ptown.y, p.point.y, prec) - @no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!) + @no_spatialite # SpatiaLite does not have an Extent function def test05_extent(self): "Testing the `extent` GeoQuerySet method." if DISABLE: return @@ -193,9 +200,10 @@ class GeoModelTest(unittest.TestCase): extent = qs.extent() for val, exp in zip(extent, expected): - self.assertAlmostEqual(exp, val, 8) + self.assertAlmostEqual(exp, val, 4) @no_oracle + @no_spatialite # SpatiaLite does not have a MakeLine function def test06_make_line(self): "Testing the `make_line` GeoQuerySet method." if DISABLE: return @@ -214,10 +222,13 @@ class GeoModelTest(unittest.TestCase): qs1 = City.objects.filter(point__disjoint=ptown.point) self.assertEqual(7, qs1.count()) - if not SpatialBackend.postgis: + if not (SpatialBackend.postgis or SpatialBackend.spatialite): # TODO: Do NULL columns bork queries on PostGIS? The following # error is encountered: # psycopg2.ProgrammingError: invalid memory alloc request size 4294957297 + # + # Similarly, on SpatiaLite Puerto Rico is also returned (could be a + # manifestation of qs2 = State.objects.filter(poly__disjoint=ptown.point) self.assertEqual(1, qs2.count()) self.assertEqual('Kansas', qs2[0].name) @@ -248,10 +259,13 @@ class GeoModelTest(unittest.TestCase): # Houston and Wellington. tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX - ks = State.objects.get(poly__contains=lawrence.point) self.assertEqual('Texas', tx.name) self.assertEqual('New Zealand', nz.name) - self.assertEqual('Kansas', ks.name) + + # Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry). + if not SpatialBackend.spatialite: + ks = State.objects.get(poly__contains=lawrence.point) + self.assertEqual('Kansas', ks.name) # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas) # are not contained in Texas or New Zealand. @@ -304,13 +318,16 @@ class GeoModelTest(unittest.TestCase): # If the GeometryField SRID is -1, then we shouldn't perform any # transformation if the SRID of the input geometry is different. - m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) - m1.save() - self.assertEqual(-1, m1.geom.srid) + # SpatiaLite does not support missing SRID values. + if not SpatialBackend.spatialite: + m1 = MinusOneSRID(geom=Point(17, 23, srid=4326)) + m1.save() + self.assertEqual(-1, m1.geom.srid) # Oracle does not support NULL geometries in its spatial index for # some routines (e.g., SDO_GEOM.RELATE). @no_oracle + @no_spatialite def test12_null_geometries(self): "Testing NULL geometry support, and the `isnull` lookup type." if DISABLE: return @@ -334,6 +351,7 @@ class GeoModelTest(unittest.TestCase): State(name='Northern Mariana Islands', poly=None).save() @no_oracle # No specific `left` or `right` operators in Oracle. + @no_spatialite # No `left` or `right` operators in SpatiaLite. def test13_left_right(self): "Testing the 'left' and 'right' lookup types." if DISABLE: return @@ -398,7 +416,7 @@ class GeoModelTest(unittest.TestCase): self.assertRaises(e, qs.count) # Relate works differently for the different backends. - if SpatialBackend.postgis: + if SpatialBackend.postgis or SpatialBackend.spatialite: contains_mask = 'T*T***FF*' within_mask = 'T*F**F***' intersects_mask = 'T********' @@ -449,9 +467,12 @@ class GeoModelTest(unittest.TestCase): union = union1 self.assertEqual(True, union.equals_exact(u1, tol)) self.assertEqual(True, union.equals_exact(u2, tol)) - qs = City.objects.filter(name='NotACity') - self.assertEqual(None, qs.unionagg(field_name='point')) + # SpatiaLite will segfault trying to union a NULL geometry. + if not SpatialBackend.spatialite: + qs = City.objects.filter(name='NotACity') + self.assertEqual(None, qs.unionagg(field_name='point')) + @no_spatialite # SpatiaLite does not support abstract geometry columns def test18_geometryfield(self): "Testing GeometryField." if DISABLE: return @@ -479,8 +500,12 @@ class GeoModelTest(unittest.TestCase): "Testing the `centroid` GeoQuerySet method." if DISABLE: return qs = State.objects.exclude(poly__isnull=True).centroid() - if SpatialBackend.oracle: tol = 0.1 - else: tol = 0.000000001 + if SpatialBackend.oracle: + tol = 0.1 + elif SpatialBackend.spatialite: + tol = 0.000001 + else: + tol = 0.000000001 for s in qs: self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol)) @@ -493,14 +518,19 @@ class GeoModelTest(unittest.TestCase): ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326), 'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326), } - elif SpatialBackend.postgis: + + elif SpatialBackend.postgis or SpatialBackend.spatialite: # Using GEOSGeometry to compute the reference point on surface values # -- since PostGIS also uses GEOS these should be the same. ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface, 'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface } for cntry in Country.objects.point_on_surface(): - self.assertEqual(ref[cntry.name], cntry.point_on_surface) + if SpatialBackend.spatialite: + # XXX This seems to be a WKT-translation-related precision issue? + tol = 0.00001 + else: tol = 0.000000001 + self.assertEqual(True, ref[cntry.name].equals_exact(cntry.point_on_surface, tol)) @no_oracle def test21_scale(self): @@ -512,8 +542,9 @@ class GeoModelTest(unittest.TestCase): for p1, p2 in zip(c.mpoly, c.scaled): for r1, r2 in zip(p1, p2): for c1, c2 in zip(r1.coords, r2.coords): - self.assertEqual(c1[0] * xfac, c2[0]) - self.assertEqual(c1[1] * yfac, c2[1]) + # XXX The low precision is for SpatiaLite + self.assertAlmostEqual(c1[0] * xfac, c2[0], 5) + self.assertAlmostEqual(c1[1] * yfac, c2[1], 5) @no_oracle def test22_translate(self): @@ -525,8 +556,9 @@ class GeoModelTest(unittest.TestCase): for p1, p2 in zip(c.mpoly, c.translated): for r1, r2 in zip(p1, p2): for c1, c2 in zip(r1.coords, r2.coords): - self.assertEqual(c1[0] + xfac, c2[0]) - self.assertEqual(c1[1] + yfac, c2[1]) + # XXX The low precision is for SpatiaLite + self.assertAlmostEqual(c1[0] + xfac, c2[0], 5) + self.assertAlmostEqual(c1[1] + yfac, c2[1], 5) def test23_numgeom(self): "Testing the `num_geom` GeoQuerySet method." @@ -539,27 +571,46 @@ class GeoModelTest(unittest.TestCase): if SpatialBackend.postgis: self.assertEqual(None, c.num_geom) else: self.assertEqual(1, c.num_geom) + @no_spatialite # SpatiaLite can only count vertices in LineStrings def test24_numpoints(self): "Testing the `num_points` GeoQuerySet method." if DISABLE: return - for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points) - if SpatialBackend.postgis: + for c in Country.objects.num_points(): + self.assertEqual(c.mpoly.num_points, c.num_points) + if not SpatialBackend.oracle: # Oracle cannot count vertices in Point geometries. for c in City.objects.num_points(): self.assertEqual(1, c.num_points) - @no_oracle def test25_geoset(self): "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." if DISABLE: return geom = Point(5, 23) - for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom): - self.assertEqual(c.mpoly.difference(geom), c.difference) - self.assertEqual(c.mpoly.intersection(geom), c.intersection) - self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference) - self.assertEqual(c.mpoly.union(geom), c.union) + tol = 1 + qs = Country.objects.all().difference(geom).sym_difference(geom).union(geom) + + # XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also, + # XXX it doesn't like the null intersection. + if SpatialBackend.spatialite: + qs = qs.exclude(name='Texas') + else: + qs = qs.intersection(geom) + + for c in qs: + if SpatialBackend.oracle: + # Should be able to execute the queries; however, they won't be the same + # as GEOS (because Oracle doesn't use GEOS internally like PostGIS or + # SpatiaLite). + pass + else: + self.assertEqual(c.mpoly.difference(geom), c.difference) + if not SpatialBackend.spatialite: + self.assertEqual(c.mpoly.intersection(geom), c.intersection) + self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference) + self.assertEqual(c.mpoly.union(geom), c.union) def test26_inherited_geofields(self): "Test GeoQuerySet methods on inherited Geometry fields." + if DISABLE: return # Creating a Pennsylvanian city. mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)') diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index fa54b90ce9a..56a9f2bdfea 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -7,9 +7,9 @@ from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, I from django.contrib.gis.gdal import DataSource shp_path = os.path.dirname(__file__) -city_shp = os.path.join(shp_path, 'cities/cities.shp') -co_shp = os.path.join(shp_path, 'counties/counties.shp') -inter_shp = os.path.join(shp_path, 'interstates/interstates.shp') +city_shp = os.path.join(shp_path, '../data/cities/cities.shp') +co_shp = os.path.join(shp_path, '../data/counties/counties.shp') +inter_shp = os.path.join(shp_path, '../data/interstates/interstates.shp') # Dictionaries to hold what's expected in the county shapefile. NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] @@ -53,7 +53,6 @@ class LayerMapTest(unittest.TestCase): def test02_simple_layermap(self): "Test LayerMapping import of a simple point shapefile." - # Setting up for the LayerMapping. lm = LayerMapping(City, city_shp, city_mapping) lm.save() @@ -78,7 +77,6 @@ class LayerMapTest(unittest.TestCase): def test03_layermap_strict(self): "Testing the `strict` keyword, and import of a LineString shapefile." - # When the `strict` keyword is set an error encountered will force # the importation to stop. try: @@ -118,7 +116,6 @@ class LayerMapTest(unittest.TestCase): def county_helper(self, county_feat=True): "Helper function for ensuring the integrity of the mapped County models." - for name, n, st in zip(NAMES, NUMS, STATES): # Should only be one record b/c of `unique` keyword. c = County.objects.get(name=name) @@ -198,7 +195,6 @@ class LayerMapTest(unittest.TestCase): def test05_test_fid_range_step(self): "Tests the `fid_range` keyword and the `step` keyword of .save()." - # Function for clearing out all the counties before testing. def clear_counties(): County.objects.all().delete() diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 0cde7cc6a77..0f2c4b83ee7 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -2,7 +2,7 @@ import os, unittest from django.contrib.gis.geos import * from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.db.models import F, Extent, Union -from django.contrib.gis.tests.utils import no_mysql, no_oracle +from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite from django.conf import settings from models import City, Location, DirectoryEntry, Parcel @@ -12,7 +12,7 @@ cities = (('Aurora', 'TX', -97.516111, 33.058333), ) class RelatedGeoModelTest(unittest.TestCase): - + def test01_setup(self): "Setting up for related model tests." for name, state, lon, lat in cities: @@ -32,7 +32,7 @@ class RelatedGeoModelTest(unittest.TestCase): self.assertEqual(nm, c.name) self.assertEqual(st, c.state) self.assertEqual(Point(lon, lat), c.location.point) - + @no_mysql @no_oracle # Pagination problem is implicated in this test as well. def test03_transform_related(self): @@ -43,7 +43,7 @@ class RelatedGeoModelTest(unittest.TestCase): tol = 3 else: tol = 0 - + def check_pnt(ref, pnt): self.assertAlmostEqual(ref.x, pnt.x, tol) self.assertAlmostEqual(ref.y, pnt.y, tol) @@ -59,14 +59,14 @@ class RelatedGeoModelTest(unittest.TestCase): # Doing this implicitly sets `select_related` select the location. # TODO: Fix why this breaks on Oracle. qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point')) - check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) + check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point) @no_mysql - def test04_related_aggregate(self): - "Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models." - + @no_spatialite + def test04a_related_extent_aggregate(self): + "Testing the `extent` GeoQuerySet aggregates on related geographic models." # This combines the Extent and Union aggregates into one query - aggs = City.objects.aggregate(Extent('location__point'), Union('location__point')) + aggs = City.objects.aggregate(Extent('location__point')) # One for all locations, one that excludes Roswell. all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633) @@ -81,14 +81,20 @@ class RelatedGeoModelTest(unittest.TestCase): for ref, e in [(all_extent, e1), (txpa_extent, e2), (all_extent, e3)]: for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val, tol) + @no_mysql + def test04b_related_union_aggregate(self): + "Testing the `unionagg` GeoQuerySet aggregates on related geographic models." + # This combines the Extent and Union aggregates into one query + aggs = City.objects.aggregate(Union('location__point')) + # These are the points that are components of the aggregate geographic # union that is returned. p1 = Point(-104.528056, 33.387222) p2 = Point(-97.516111, 33.058333) p3 = Point(-79.460734, 40.18476) - + # Creating the reference union geometry depending on the spatial backend, - # as Oracle will have a different internal ordering of the component + # as Oracle will have a different internal ordering of the component # geometries than PostGIS. The second union aggregate is for a union # query that includes limiting information in the WHERE clause (in other # words a `.filter()` precedes the call to `.unionagg()`). @@ -98,7 +104,7 @@ class RelatedGeoModelTest(unittest.TestCase): else: ref_u1 = MultiPoint(p1, p2, p3, srid=4326) ref_u2 = MultiPoint(p2, p3, srid=4326) - + u1 = City.objects.unionagg(field_name='location__point') u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point') u3 = aggs['location__point__union'] @@ -106,7 +112,7 @@ class RelatedGeoModelTest(unittest.TestCase): self.assertEqual(ref_u1, u1) self.assertEqual(ref_u2, u2) self.assertEqual(ref_u1, u3) - + def test05_select_related_fk_to_subclass(self): "Testing that calling select_related on a query over a model with an FK to a model subclass works" # Regression test for #9752. @@ -140,14 +146,14 @@ class RelatedGeoModelTest(unittest.TestCase): qs = Parcel.objects.filter(center1__within=F('border1')) self.assertEqual(1, len(qs)) self.assertEqual('P2', qs[0].name) - + if not SpatialBackend.mysql: # This time center2 is in a different coordinate system and needs # to be wrapped in transformation SQL. qs = Parcel.objects.filter(center2__within=F('border1')) self.assertEqual(1, len(qs)) self.assertEqual('P2', qs[0].name) - + # Should return the first Parcel, which has the center point equal # to the point in the City ForeignKey. qs = Parcel.objects.filter(center1=F('city__location__point')) diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py index 0b368f13855..b245b2c22fa 100644 --- a/django/contrib/gis/tests/utils.py +++ b/django/contrib/gis/tests/utils.py @@ -15,8 +15,10 @@ def no_backend(test_func, backend): def no_oracle(func): return no_backend(func, 'oracle') def no_postgis(func): return no_backend(func, 'postgresql_psycopg2') def no_mysql(func): return no_backend(func, 'mysql') +def no_spatialite(func): return no_backend(func, 'sqlite3') # Shortcut booleans to omit only portions of tests. oracle = settings.DATABASE_ENGINE == 'oracle' postgis = settings.DATABASE_ENGINE == 'postgresql_psycopg2' mysql = settings.DATABASE_ENGINE == 'mysql' +spatialite = settings.DATABASE_ENGINE == 'sqlite3'