summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Bronn <jbronn@gmail.com>2008-06-15 19:48:57 +0000
committerJustin Bronn <jbronn@gmail.com>2008-06-15 19:48:57 +0000
commit4ec80c4333b618fc1ef2a02c7b8ca8719792f25f (patch)
tree13a4df36225a817215cd3af2ad7dabd28778824f
parenteb30cad66ab1d5ff6a56425ecdf7efd0ade6d750 (diff)
gis: Refactor of the `GeoQuerySet`; new features include:
(1) Creation of internal API that eases generation of `GeoQuerySet` methods. (2) `GeoQuerySet.distance` now returns `Distance` objects instead of floats. (3) Added the new `GeoQuerySet` methods: `area`, `centroid`, `difference`, `envelope`, `intersection`, `length`, `make_line`, `mem_size`, `num_geom`, `num_points`, `perimeter`, `point_on_surface`, `scale`, `svg`, `sym_difference`, `translate`, `union`. (4) The `model_att` keyword may be used to customize the attribute that `GeoQuerySet` methods attach output to. (5) Geographic distance lookups and `GeoQuerySet.distance` calls now use `ST_distance_sphere` by default (performance benefits far outweigh small loss in accuracy); `ST_distance_spheroid` may still be used by specifying an option. (6) `GeoQuerySet` methods may now operate accross ForeignKey relations specified via the `field_name` keyword (but this does not work on Oracle). (7) `Area` now has the same units of measure as `Distance`. Backward Incompatibilites: * The aggregate union method is now known as `unionagg`. * The `field_name` keyword used for `GeoQuerySet` methods may no longer be specified via positional arguments. * `Distance` objects returned instead of floats from `GeoQuerySet.distance`. * `ST_Distance_sphere` used by default for geographic distance calculations. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7641 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/contrib/gis/db/backend/__init__.py87
-rw-r--r--django/contrib/gis/db/backend/base.py29
-rw-r--r--django/contrib/gis/db/backend/mysql/__init__.py12
-rw-r--r--django/contrib/gis/db/backend/oracle/__init__.py31
-rw-r--r--django/contrib/gis/db/backend/oracle/adaptor.py5
-rw-r--r--django/contrib/gis/db/backend/oracle/query.py13
-rw-r--r--django/contrib/gis/db/backend/postgis/__init__.py42
-rw-r--r--django/contrib/gis/db/backend/postgis/field.py1
-rw-r--r--django/contrib/gis/db/backend/postgis/query.py66
-rw-r--r--django/contrib/gis/db/models/fields/__init__.py74
-rw-r--r--django/contrib/gis/db/models/manager.py51
-rw-r--r--django/contrib/gis/db/models/proxy.py8
-rw-r--r--django/contrib/gis/db/models/query.py677
-rw-r--r--django/contrib/gis/db/models/sql/__init__.py2
-rw-r--r--django/contrib/gis/db/models/sql/query.py104
-rw-r--r--django/contrib/gis/db/models/sql/where.py4
-rw-r--r--django/contrib/gis/measure.py129
-rw-r--r--django/contrib/gis/models.py47
-rw-r--r--django/contrib/gis/tests/__init__.py13
-rw-r--r--django/contrib/gis/tests/distapp/data.py27
-rw-r--r--django/contrib/gis/tests/distapp/models.py30
-rw-r--r--django/contrib/gis/tests/distapp/tests.py248
-rw-r--r--django/contrib/gis/tests/geoapp/tests.py160
-rw-r--r--django/contrib/gis/tests/geoapp/tests_mysql.py6
-rw-r--r--django/contrib/gis/tests/layermap/models.py4
-rw-r--r--django/contrib/gis/tests/layermap/tests.py2
-rw-r--r--django/contrib/gis/tests/relatedapp/tests.py29
27 files changed, 1362 insertions, 539 deletions
diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py
index cb027b9eec..172c1268a7 100644
--- a/django/contrib/gis/db/backend/__init__.py
+++ b/django/contrib/gis/db/backend/__init__.py
@@ -3,97 +3,16 @@
Specifically, this module will import the correct routines and modules
needed for GeoDjango to interface with the spatial database.
-
- Some of the more important classes and routines from the spatial backend
- include:
-
- (1) `GeoBackEndField`, a base class needed for GeometryField.
- (2) `get_geo_where_clause`, a routine used by `GeoWhereNode`.
- (3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types.
- (4) `SpatialBackend`, a container object for information specific to the
- spatial backend.
"""
from django.conf import settings
-from django.db.models.sql.query import QUERY_TERMS
from django.contrib.gis.db.backend.util import gqn
-# These routines (needed by GeoManager), default to False.
-ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
-
-# Lookup types in which the rest of the parameters are not
-# needed to be substitute in the WHERE SQL (e.g., the 'relate'
-# operation on Oracle does not need the mask substituted back
-# into the query SQL.).
-LIMITED_WHERE = []
-
# Retrieving the necessary settings from the backend.
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
- from django.contrib.gis.db.backend.postgis.adaptor import \
- PostGISAdaptor as GeoAdaptor
- from django.contrib.gis.db.backend.postgis.field import \
- PostGISField as GeoBackendField
- from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
- from django.contrib.gis.db.backend.postgis.query import \
- get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
- ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
- EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
- MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
- # PostGIS version info is needed to determine calling order of some
- # stored procedures (e.g., AsGML()).
- VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
- SPATIAL_BACKEND = 'postgis'
+ from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'oracle':
- from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
- from django.contrib.gis.db.backend.oracle.field import \
- OracleSpatialField as GeoBackendField
- from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
- from django.contrib.gis.db.backend.oracle.query import \
- get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
- ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
- SPATIAL_BACKEND = 'oracle'
- LIMITED_WHERE = ['relate']
+ from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
elif settings.DATABASE_ENGINE == 'mysql':
- from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
- from django.contrib.gis.db.backend.mysql.field import \
- MySQLGeoField as GeoBackendField
- from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
- from django.contrib.gis.db.backend.mysql.query import \
- get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
- DISTANCE_FUNCTIONS = {}
- SPATIAL_BACKEND = 'mysql'
+ from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
-
-class SpatialBackend(object):
- "A container for properties of the SpatialBackend."
- # Stored procedure names used by the `GeoManager`.
- as_kml = ASKML
- as_gml = ASGML
- distance = DISTANCE
- distance_spheroid = DISTANCE_SPHEROID
- extent = EXTENT
- name = SPATIAL_BACKEND
- select = GEOM_SELECT
- transform = TRANSFORM
- union = UNION
-
- # Version information, if defined.
- version = VERSION
-
- # All valid GIS lookup terms, and distance functions.
- gis_terms = GIS_TERMS
- distance_functions = DISTANCE_FUNCTIONS
-
- # Lookup types where additional WHERE parameters are excluded.
- limited_where = LIMITED_WHERE
-
- # Shortcut booleans.
- mysql = SPATIAL_BACKEND == 'mysql'
- oracle = SPATIAL_BACKEND == 'oracle'
- postgis = SPATIAL_BACKEND == 'postgis'
-
- # Class for the backend field.
- Field = GeoBackendField
-
- # Adaptor class used for quoting GEOS geometries in the database.
- Adaptor = GeoAdaptor
diff --git a/django/contrib/gis/db/backend/base.py b/django/contrib/gis/db/backend/base.py
new file mode 100644
index 0000000000..d45ac7b6f1
--- /dev/null
+++ b/django/contrib/gis/db/backend/base.py
@@ -0,0 +1,29 @@
+"""
+ This module holds the base `SpatialBackend` object, which is
+ instantiated by each spatial backend with the features it has.
+"""
+# TODO: Create a `Geometry` protocol and allow user to use
+# different Geometry objects -- for now we just use GEOSGeometry.
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+
+class BaseSpatialBackend(object):
+ Geometry = GEOSGeometry
+ GeometryException = GEOSException
+
+ def __init__(self, **kwargs):
+ kwargs.setdefault('distance_functions', {})
+ kwargs.setdefault('limited_where', {})
+ for k, v in kwargs.iteritems(): setattr(self, k, v)
+
+ def __getattr__(self, name):
+ """
+ All attributes of the spatial backend return False by default.
+ """
+ try:
+ 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 8b13789179..0484e5f9b2 100644
--- a/django/contrib/gis/db/backend/mysql/__init__.py
+++ b/django/contrib/gis/db/backend/mysql/__init__.py
@@ -1 +1,13 @@
+__all__ = ['create_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.field import MySQLGeoField
+from django.contrib.gis.db.backend.mysql.query import *
+
+SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
+ gis_terms=MYSQL_GIS_TERMS,
+ select=GEOM_SELECT,
+ Adaptor=WKTAdaptor,
+ Field=MySQLGeoField)
diff --git a/django/contrib/gis/db/backend/oracle/__init__.py b/django/contrib/gis/db/backend/oracle/__init__.py
index e69de29bb2..3eee56ea23 100644
--- a/django/contrib/gis/db/backend/oracle/__init__.py
+++ b/django/contrib/gis/db/backend/oracle/__init__.py
@@ -0,0 +1,31 @@
+__all__ = ['create_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.field import OracleSpatialField
+from django.contrib.gis.db.backend.oracle.query import *
+
+SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
+ area=AREA,
+ centroid=CENTROID,
+ difference=DIFFERENCE,
+ distance=DISTANCE,
+ distance_functions=DISTANCE_FUNCTIONS,
+ gis_terms=ORACLE_SPATIAL_TERMS,
+ gml=ASGML,
+ intersection=INTERSECTION,
+ length=LENGTH,
+ limited_where = {'relate' : None},
+ num_geom=NUM_GEOM,
+ num_points=NUM_POINTS,
+ perimeter=LENGTH,
+ point_on_surface=POINT_ON_SURFACE,
+ select=GEOM_SELECT,
+ sym_difference=SYM_DIFFERENCE,
+ transform=TRANSFORM,
+ unionagg=UNIONAGG,
+ union=UNION,
+ Adaptor=OracleSpatialAdaptor,
+ Field=OracleSpatialField,
+ )
diff --git a/django/contrib/gis/db/backend/oracle/adaptor.py b/django/contrib/gis/db/backend/oracle/adaptor.py
new file mode 100644
index 0000000000..95dc265795
--- /dev/null
+++ b/django/contrib/gis/db/backend/oracle/adaptor.py
@@ -0,0 +1,5 @@
+from cx_Oracle import CLOB
+from django.contrib.gis.db.backend.adaptor import WKTAdaptor
+
+class OracleSpatialAdaptor(WKTAdaptor):
+ input_size = CLOB
diff --git a/django/contrib/gis/db/backend/oracle/query.py b/django/contrib/gis/db/backend/oracle/query.py
index d4cd3669e2..6f5c0dfe94 100644
--- a/django/contrib/gis/db/backend/oracle/query.py
+++ b/django/contrib/gis/db/backend/oracle/query.py
@@ -15,10 +15,21 @@ from django.contrib.gis.measure import Distance
qn = connection.ops.quote_name
# The GML, distance, transform, and union procedures.
+AREA = 'SDO_GEOM.SDO_AREA'
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
+CENTROID = 'SDO_GEOM.SDO_CENTROID'
+DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
+EXTENT = 'SDO_AGGR_MBR'
+INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
+LENGTH = 'SDO_GEOM.SDO_LENGTH'
+NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
+NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
+POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
+SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
TRANSFORM = 'SDO_CS.TRANSFORM'
-UNION = 'SDO_AGGR_UNION'
+UNION = 'SDO_GEOM.SDO_UNION'
+UNIONAGG = 'SDO_AGGR_UNION'
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py
index e69de29bb2..8a4d09e0d5 100644
--- a/django/contrib/gis/db/backend/postgis/__init__.py
+++ b/django/contrib/gis/db/backend/postgis/__init__.py
@@ -0,0 +1,42 @@
+__all__ = ['create_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.field import PostGISField
+from django.contrib.gis.db.backend.postgis.query import *
+
+SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
+ area=AREA,
+ centroid=CENTROID,
+ difference=DIFFERENCE,
+ distance=DISTANCE,
+ distance_functions=DISTANCE_FUNCTIONS,
+ distance_sphere=DISTANCE_SPHERE,
+ distance_spheroid=DISTANCE_SPHEROID,
+ envelope=ENVELOPE,
+ extent=EXTENT,
+ gis_terms=POSTGIS_TERMS,
+ gml=ASGML,
+ intersection=INTERSECTION,
+ kml=ASKML,
+ length=LENGTH,
+ length_spheroid=LENGTH_SPHEROID,
+ make_line=MAKE_LINE,
+ mem_size=MEM_SIZE,
+ num_geom=NUM_GEOM,
+ num_points=NUM_POINTS,
+ perimeter=PERIMETER,
+ point_on_surface=POINT_ON_SURFACE,
+ scale=SCALE,
+ select=GEOM_SELECT,
+ svg=ASSVG,
+ sym_difference=SYM_DIFFERENCE,
+ transform=TRANSFORM,
+ translate=TRANSLATE,
+ union=UNION,
+ unionagg=UNIONAGG,
+ version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
+ Adaptor=PostGISAdaptor,
+ Field=PostGISField,
+ )
diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py
index 9e20a72eee..1e055ff60b 100644
--- a/django/contrib/gis/db/backend/postgis/field.py
+++ b/django/contrib/gis/db/backend/postgis/field.py
@@ -1,6 +1,5 @@
from django.db import connection
from django.db.models.fields import Field # Django base Field class
-from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import gqn
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py
index 2610173673..90063e1887 100644
--- a/django/contrib/gis/db/backend/postgis/query.py
+++ b/django/contrib/gis/db/backend/postgis/query.py
@@ -21,7 +21,7 @@ if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
-# means that 'ST_' is prefixes geometry function names.
+# means that 'ST_' prefixes geometry function names.
GEOM_FUNC_PREFIX = ''
if MAJOR_VERSION >= 1:
if (MINOR_VERSION1 > 2 or
@@ -30,26 +30,46 @@ if MAJOR_VERSION >= 1:
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
- # Custom selection not needed for PostGIS since GEOS geometries may be
+ # Custom selection not needed for PostGIS because GEOS geometries are
# instantiated directly from the HEXEWKB returned by default. If
# WKT is needed for some reason in the future, this value may be changed,
- # 'AsText(%s)'
+ # e.g,, 'AsText(%s)'.
GEOM_SELECT = None
# Functions used by the GeoManager & GeoQuerySet
+ AREA = get_func('Area')
ASKML = get_func('AsKML')
ASGML = get_func('AsGML')
+ ASSVG = get_func('AsSVG')
+ CENTROID = get_func('Centroid')
+ DIFFERENCE = get_func('Difference')
DISTANCE = get_func('Distance')
+ DISTANCE_SPHERE = get_func('distance_sphere')
DISTANCE_SPHEROID = get_func('distance_spheroid')
+ ENVELOPE = get_func('Envelope')
EXTENT = get_func('extent')
GEOM_FROM_TEXT = get_func('GeomFromText')
GEOM_FROM_WKB = get_func('GeomFromWKB')
+ INTERSECTION = get_func('Intersection')
+ LENGTH = get_func('Length')
+ LENGTH_SPHEROID = get_func('length_spheroid')
+ MAKE_LINE = get_func('MakeLine')
+ MEM_SIZE = get_func('mem_size')
+ NUM_GEOM = get_func('NumGeometries')
+ NUM_POINTS = get_func('npoints')
+ PERIMETER = get_func('Perimeter')
+ POINT_ON_SURFACE = get_func('PointOnSurface')
+ SCALE = get_func('Scale')
+ SYM_DIFFERENCE = get_func('SymDifference')
TRANSFORM = get_func('Transform')
+ TRANSLATE = get_func('Translate')
# Special cases for union and KML methods.
if MINOR_VERSION1 < 3:
- UNION = 'GeomUnion'
+ UNIONAGG = 'GeomUnion'
+ UNION = 'Union'
else:
+ UNIONAGG = 'ST_Union'
UNION = 'ST_Union'
if MINOR_VERSION1 == 1:
@@ -80,16 +100,23 @@ class PostGISDistance(PostGISFunction):
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
operator=operator, result='%%s')
-class PostGISSphereDistance(PostGISFunction):
- "For PostGIS spherical distance operations."
+class PostGISSpheroidDistance(PostGISFunction):
+ "For PostGIS spherical distance operations (using the spheroid)."
dist_func = 'distance_spheroid'
def __init__(self, operator):
# An extra parameter in `end_subst` is needed for the spheroid string.
- super(PostGISSphereDistance, self).__init__(self.dist_func,
- beg_subst='%s(%s, %%s, %%s',
- end_subst=') %s %s',
- operator=operator, result='%%s')
+ super(PostGISSpheroidDistance, self).__init__(self.dist_func,
+ beg_subst='%s(%s, %%s, %%s',
+ end_subst=') %s %s',
+ operator=operator, result='%%s')
+class PostGISSphereDistance(PostGISFunction):
+ "For PostGIS spherical distance operations."
+ dist_func = 'distance_sphere'
+ def __init__(self, operator):
+ super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
+ operator=operator, result='%%s')
+
class PostGISRelate(PostGISFunctionParam):
"For PostGIS Relate(<geom>, <pattern>) calls."
pattern_regex = re.compile(r'^[012TF\*]{9}$')
@@ -164,7 +191,7 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
dtypes = (Decimal, Distance, float, int, long)
def get_dist_ops(operator):
"Returns operations for both regular and spherical distances."
- return (PostGISDistance(operator), PostGISSphereDistance(operator))
+ return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
DISTANCE_FUNCTIONS = {
'distance_gt' : (get_dist_ops('>'), dtypes),
'distance_gte' : (get_dist_ops('>='), dtypes),
@@ -193,6 +220,13 @@ POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Func
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
+# For checking tuple parameters -- not very pretty but gets job done.
+def exactly_two(val): return val == 2
+def two_to_three(val): return val >= 2 and val <=3
+def num_params(lookup_type, val):
+ if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
+ else: return exactly_two(val)
+
#### The `get_geo_where_clause` function for PostGIS. ####
def get_geo_where_clause(lookup_type, table_prefix, field, value):
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
@@ -216,8 +250,10 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, (tuple, list)):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
- if len(value) != 2:
- raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
+ # Number of valid tuple parameters depends on the lookup type.
+ nparams = len(value)
+ if not num_params(lookup_type, nparams):
+ raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
@@ -234,7 +270,9 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
if value[0].geom_typeid != 0:
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
- op = op[1]
+ # Setting up the geodetic operation appropriately.
+ if nparams == 3 and value[2] == 'spheroid': op = op[2]
+ else: op = op[1]
else:
op = op[0]
else:
diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py
index f36045cf02..0a0d144dcc 100644
--- a/django/contrib/gis/db/models/fields/__init__.py
+++ b/django/contrib/gis/db/models/fields/__init__.py
@@ -3,15 +3,12 @@ from django.db import connection
from django.contrib.gis.db.backend import SpatialBackend, gqn
# GeometryProxy, GEOS, Distance, and oldforms imports.
from django.contrib.gis.db.models.proxy import GeometryProxy
-from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.measure import Distance
from django.contrib.gis.oldforms import WKTField
-# Attempting to get the spatial reference system.
-try:
- from django.contrib.gis.models import SpatialRefSys
-except ImportError:
- SpatialRefSys = None
+# The `get_srid_info` function gets SRID information from the spatial
+# 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.
class GeometryField(SpatialBackend.Field):
@@ -47,29 +44,7 @@ class GeometryField(SpatialBackend.Field):
# Setting the SRID and getting the units. Unit information must be
# easily available in the field instance for distance queries.
self._srid = srid
- if SpatialRefSys:
- # Getting the spatial reference WKT associated with the SRID from the
- # `spatial_ref_sys` (or equivalent) spatial database table.
- #
- # The following doesn't work: SpatialRefSys.objects.get(srid=srid)
- # Why? `syncdb` fails to recognize installed geographic models when there's
- # an ORM query instantiated within a model field.
- 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)
- srs_wkt = cur.fetchone()[0]
-
- # Getting metadata associated with the spatial reference system identifier.
- # Specifically, getting the unit information and spheroid information
- # (both required for distance queries).
- self._unit, self._unit_name = SpatialRefSys.get_units(srs_wkt)
- self._spheroid = SpatialRefSys.get_spheroid(srs_wkt)
+ self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
# Setting the dimension of the geometry field.
self._dim = dim
@@ -79,19 +54,26 @@ class GeometryField(SpatialBackend.Field):
### Routines specific to GeometryField ###
@property
def geodetic(self):
+ """
+ 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
- def get_distance(self, dist, lookup_type):
+ def get_distance(self, dist_val, lookup_type):
"""
Returns a distance number in units of the field. For example, if
`D(km=1)` was passed in and the units of the field were in meters,
then 1000 would be returned.
"""
- postgis = SpatialBackend.name == 'postgis'
+ # Getting the distance parameter and any options.
+ if len(dist_val) == 1: dist, option = dist_val[0], None
+ else: dist, option = dist_val
+
if isinstance(dist, Distance):
if self.geodetic:
# Won't allow Distance objects w/DWithin lookups on PostGIS.
- if postgis and lookup_type == 'dwithin':
+ if SpatialBackend.postgis and lookup_type == 'dwithin':
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
# Spherical distance calculation parameter should be in meters.
dist_param = dist.m
@@ -100,9 +82,11 @@ class GeometryField(SpatialBackend.Field):
else:
# Assuming the distance is in the units of the field.
dist_param = dist
-
- # Sphereical distance query; returning meters.
- if postgis and self.geodetic and lookup_type != 'dwithin':
+
+ if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
+ # On PostGIS, by default `ST_distance_sphere` is used; but if the
+ # accuracy of `ST_distance_spheroid` is needed than the spheroid
+ # needs to be passed to the SQL stored procedure.
return [gqn(self._spheroid), dist_param]
else:
return [dist_param]
@@ -119,12 +103,12 @@ class GeometryField(SpatialBackend.Field):
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
- if isinstance(geom, GEOSGeometry):
+ if isinstance(geom, SpatialBackend.Geometry):
pass
elif isinstance(geom, basestring):
try:
- geom = GEOSGeometry(geom)
- except GEOSException:
+ geom = SpatialBackend.Geometry(geom)
+ except SpatialBackend.GeometryException:
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
else:
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
@@ -148,8 +132,8 @@ class GeometryField(SpatialBackend.Field):
def contribute_to_class(self, cls, name):
super(GeometryField, self).contribute_to_class(cls, name)
- # Setup for lazy-instantiated GEOSGeometry object.
- setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
+ # Setup for lazy-instantiated Geometry object.
+ setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
def get_db_prep_lookup(self, lookup_type, value):
"""
@@ -166,7 +150,7 @@ class GeometryField(SpatialBackend.Field):
geom = self.get_geometry(value)
# Getting the WHERE clause list and the associated params list. The params
- # list is populated with the Adaptor wrapping the GEOSGeometry for the
+ # list is populated with the Adaptor wrapping the Geometry for the
# backend. The WHERE clause list contains the placeholder for the adaptor
# (e.g. any transformation SQL).
where = [self.get_placeholder(geom)]
@@ -175,7 +159,7 @@ class GeometryField(SpatialBackend.Field):
if isinstance(value, (tuple, list)):
if lookup_type in SpatialBackend.distance_functions:
# Getting the distance parameter in the units of the field.
- where += self.get_distance(value[1], lookup_type)
+ where += self.get_distance(value[1:], lookup_type)
elif lookup_type in SpatialBackend.limited_where:
pass
else:
@@ -187,15 +171,15 @@ class GeometryField(SpatialBackend.Field):
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
- if isinstance(value, GEOSGeometry):
+ if isinstance(value, SpatialBackend.Geometry):
return SpatialBackend.Adaptor(value)
elif value is None:
return None
else:
- raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
+ raise TypeError('Geometry Proxy should only return Geometry objects or None.')
def get_manipulator_field_objs(self):
- "Using the WKTField (defined above) to be our manipulator."
+ "Using the WKTField (oldforms) to be our manipulator."
return [WKTField]
# The OpenGIS Geometry Type Fields
diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py
index 8634dd9cee..4b010ac20c 100644
--- a/django/contrib/gis/db/models/manager.py
+++ b/django/contrib/gis/db/models/manager.py
@@ -7,20 +7,71 @@ class GeoManager(Manager):
def get_query_set(self):
return GeoQuerySet(model=self.model)
+ def area(self, *args, **kwargs):
+ return self.get_query_set().area(*args, **kwargs)
+
+ def centroid(self, *args, **kwargs):
+ return self.get_query_set().centroid(*args, **kwargs)
+
+ def difference(self, *args, **kwargs):
+ return self.get_query_set().difference(*args, **kwargs)
+
def distance(self, *args, **kwargs):
return self.get_query_set().distance(*args, **kwargs)
+ def envelope(self, *args, **kwargs):
+ return self.get_query_set().envelope(*args, **kwargs)
+
def extent(self, *args, **kwargs):
return self.get_query_set().extent(*args, **kwargs)
def gml(self, *args, **kwargs):
return self.get_query_set().gml(*args, **kwargs)
+ def intersection(self, *args, **kwargs):
+ return self.get_query_set().intersection(*args, **kwargs)
+
def kml(self, *args, **kwargs):
return self.get_query_set().kml(*args, **kwargs)
+ def length(self, *args, **kwargs):
+ return self.get_query_set().length(*args, **kwargs)
+
+ def make_line(self, *args, **kwargs):
+ return self.get_query_set().make_line(*args, **kwargs)
+
+ def mem_size(self, *args, **kwargs):
+ return self.get_query_set().mem_size(*args, **kwargs)
+
+ def num_geom(self, *args, **kwargs):
+ return self.get_query_set().num_geom(*args, **kwargs)
+
+ def num_points(self, *args, **kwargs):
+ return self.get_query_set().num_points(*args, **kwargs)
+
+ def perimeter(self, *args, **kwargs):
+ return self.get_query_set().perimeter(*args, **kwargs)
+
+ def point_on_surface(self, *args, **kwargs):
+ return self.get_query_set().point_on_surface(*args, **kwargs)
+
+ def scale(self, *args, **kwargs):
+ return self.get_query_set().scale(*args, **kwargs)
+
+ def svg(self, *args, **kwargs):
+ return self.get_query_set().svg(*args, **kwargs)
+
+ def sym_difference(self, *args, **kwargs):
+ return self.get_query_set().sym_difference(*args, **kwargs)
+
def transform(self, *args, **kwargs):
return self.get_query_set().transform(*args, **kwargs)
+ def translate(self, *args, **kwargs):
+ return self.get_query_set().translate(*args, **kwargs)
+
def union(self, *args, **kwargs):
return self.get_query_set().union(*args, **kwargs)
+
+ def unionagg(self, *args, **kwargs):
+ return self.get_query_set().unionagg(*args, **kwargs)
diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py
index 43e7dbe8ef..34276a6d63 100644
--- a/django/contrib/gis/db/models/proxy.py
+++ b/django/contrib/gis/db/models/proxy.py
@@ -1,7 +1,7 @@
"""
The GeometryProxy object, allows for lazy-geometries. The proxy uses
- Python descriptors for instantiating and setting GEOS Geometry objects
- corresponding to geographic model fields.
+ Python descriptors for instantiating and setting Geometry objects
+ corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322).
"""
@@ -31,8 +31,8 @@ class GeometryProxy(object):
elif (geom_value is None) or (geom_value==''):
geom = None
else:
- # Otherwise, a GEOSGeometry object is built using the field's contents,
- # and the model's corresponding attribute is set.
+ # Otherwise, a Geometry object is built using the field's contents,
+ # and the model's corresponding attribute is set.
geom = self._klass(geom_value)
setattr(obj, self._field.attname, geom)
return geom
diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py
index 65b011c7a8..e779886c92 100644
--- a/django/contrib/gis/db/models/query.py
+++ b/django/contrib/gis/db/models/query.py
@@ -1,12 +1,12 @@
-from itertools import izip
from django.core.exceptions import ImproperlyConfigured
from django.db import connection
from django.db.models.query import sql, QuerySet, Q
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField, PointField
-from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode
-from django.contrib.gis.geos import GEOSGeometry, Point
+from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
+from django.contrib.gis.measure import Area, Distance
+from django.contrib.gis.models import get_srid_info
qn = connection.ops.quote_name
# For backwards-compatibility; Q object should work just fine
@@ -28,174 +28,236 @@ class GeoQuerySet(QuerySet):
super(GeoQuerySet, self).__init__(model=model, query=query)
self.query = query or GeoQuery(self.model, connection)
- def distance(self, *args, **kwargs):
+ def area(self, tolerance=0.05, **kwargs):
"""
- Returns the distance from the given geographic field name to the
- given geometry in a `distance` attribute on each element of the
- GeoQuerySet.
+ Returns the area of the geographic field in an `area` attribute on
+ each element of this GeoQuerySet.
"""
- DISTANCE = SpatialBackend.distance
- if not DISTANCE:
- raise ImproperlyConfigured('Distance() stored proecedure not available.')
-
- # Getting the geometry field and GEOSGeometry object to base distance
- # calculations from.
- nargs = len(args)
- if nargs == 1:
- field_name = None
- geom = args[0]
- elif nargs == 2:
- field_name, geom = args
- else:
- raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
-
- # Getting the GeometryField and quoted column.
- geo_field = self.query._geo_field(field_name)
- if not geo_field:
- raise TypeError('Distance output only available on GeometryFields.')
- geo_col = self.query._field_column(geo_field)
-
- # Using the field's get_db_prep_lookup() to get any needed
- # transformation SQL -- we pass in a 'dummy' `contains`
- # `distance_lte` lookup type.
- where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
+ # Peforming setup here rather than in `_spatial_attribute` so that
+ # we can get the units for `AreaField`.
+ procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
+ s = {'procedure_args' : procedure_args,
+ 'geo_field' : geo_field,
+ 'setup' : False,
+ }
if SpatialBackend.oracle:
- # The `tolerance` keyword may be used for Oracle; the tolerance is
- # in meters -- a default of 5 centimeters is used.
- tolerance = kwargs.get('tolerance', 0.05)
- dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)}
- else:
- if len(where) == 3:
- # Spherical distance calculation was requested (b/c spheroid
- # parameter was attached) However, the PostGIS ST_distance_spheroid()
- # procedure 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.')
- if not isinstance(GEOSGeometry(buffer(params[0].wkb)), Point):
- raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
-
- # Call to distance_spheroid() requires the spheroid as well.
- dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, where[0], where[1])
+ 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:
+ if not geo_field.geodetic:
+ # Getting the area units of the geographic field.
+ s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
else:
- dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0])
- dist_select = {'distance' : dist_sql}
- return self.extra(select=dist_select, select_params=params)
+ # TODO: Do we want to support raw number areas for geodetic fields?
+ raise Exception('Area on geodetic coordinate systems not supported.')
+ return self._spatial_attribute('area', s, **kwargs)
- def extent(self, field_name=None):
+ def centroid(self, **kwargs):
"""
- Returns the extent (aggregate) of the features in the GeoQuerySet. The
- extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
+ Returns the centroid of the geographic field in a `centroid`
+ attribute on each element of this GeoQuerySet.
"""
- EXTENT = SpatialBackend.extent
- if not EXTENT:
- raise ImproperlyConfigured('Extent stored procedure not available.')
+ return self._geom_attribute('centroid', **kwargs)
- # Getting the GeometryField and quoted column.
- geo_field = self.query._geo_field(field_name)
- if not geo_field:
- raise TypeError('Extent information only available on GeometryFields.')
- geo_col = self.query._field_column(geo_field)
+ def difference(self, geom, **kwargs):
+ """
+ Returns the spatial difference of the geographic field in a `difference`
+ attribute on each element of this GeoQuerySet.
+ """
+ return self._geomset_attribute('difference', geom, **kwargs)
- # Constructing the query that will select the extent.
- extent_sql = '%s(%s)' % (EXTENT, geo_col)
+ def distance(self, geom, **kwargs):
+ """
+ Returns the distance from the given geographic field name to the
+ given geometry in a `distance` attribute on each element of the
+ GeoQuerySet.
- self.query.select = [GeomSQL(extent_sql)]
- self.query.select_fields = [None]
- try:
- esql, params = self.query.as_sql()
- except sql.datastructures.EmptyResultSet:
- return None
+ Keyword Arguments:
+ `spheroid` => If the geometry field is geodetic and PostGIS is
+ the spatial database, then the more accurate
+ spheroid calculation will be used instead of the
+ quicker sphere calculation.
+
+ `tolerance` => Used only for Oracle. The tolerance is
+ in meters -- a default of 5 centimeters (0.05)
+ is used.
+ """
+ return self._distance_attribute('distance', geom, **kwargs)
- # Getting a cursor, executing the query, and extracting the returned
- # value from the extent function.
- cursor = connection.cursor()
- cursor.execute(esql, params)
- box = cursor.fetchone()[0]
+ def envelope(self, **kwargs):
+ """
+ Returns a Geometry representing the bounding box of the
+ Geometry field in an `envelope` attribute on each element of
+ the GeoQuerySet.
+ """
+ return self._geom_attribute('envelope', **kwargs)
- if box:
- # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
- # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
- # parsing out and returning as a 4-tuple.
- ll, ur = box[4:-1].split(',')
- xmin, ymin = map(float, ll.split())
- xmax, ymax = map(float, ur.split())
- return (xmin, ymin, xmax, ymax)
- else:
- return None
+ def extent(self, **kwargs):
+ """
+ Returns the extent (aggregate) of the features in the GeoQuerySet. The
+ extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
+ """
+ convert_extent = None
+ if SpatialBackend.postgis:
+ def convert_extent(box, geo_field):
+ # TODO: Parsing of BOX3D, Oracle support (patches welcome!)
+ # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
+ # parsing out and returning as a 4-tuple.
+ ll, ur = box[4:-1].split(',')
+ xmin, ymin = map(float, ll.split())
+ xmax, ymax = map(float, ur.split())
+ return (xmin, ymin, xmax, ymax)
+ elif SpatialBackend.oracle:
+ def convert_extent(wkt, geo_field):
+ raise NotImplementedError
+ return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
- def gml(self, field_name=None, precision=8, version=2):
+ def gml(self, precision=8, version=2, **kwargs):
"""
Returns GML representation of the given field in a `gml` attribute
on each element of the GeoQuerySet.
"""
- # Is GML output supported?
- ASGML = SpatialBackend.as_gml
- if not ASGML:
- raise ImproperlyConfigured('AsGML() stored procedure not available.')
-
- # Getting the GeometryField and quoted column.
- geo_field = self.query._geo_field(field_name)
- if not geo_field:
- raise TypeError('GML output only available on GeometryFields.')
- geo_col = self.query._field_column(geo_field)
-
- if SpatialBackend.oracle:
- gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
- elif SpatialBackend.postgis:
+ s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
+ if SpatialBackend.postgis:
# PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh.
major, minor1, minor2 = SpatialBackend.version
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
- gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, geo_col, precision)}
+ procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
else:
- gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
+ procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
+ s['procedure_args'] = {'precision' : precision, 'version' : version}
- # Adding GML function call to SELECT part of the SQL.
- return self.extra(select=gml_select)
+ return self._spatial_attribute('gml', s, **kwargs)
- def kml(self, field_name=None, precision=8):
+ def intersection(self, geom, **kwargs):
"""
- Returns KML representation of the given field name in a `kml`
- attribute on each element of the GeoQuerySet.
+ Returns the spatial intersection of the Geometry field in
+ an `intersection` attribute on each element of this
+ GeoQuerySet.
"""
- # Is KML output supported?
- ASKML = SpatialBackend.as_kml
- if not ASKML:
- raise ImproperlyConfigured('AsKML() stored procedure not available.')
+ return self._geomset_attribute('intersection', geom, **kwargs)
- # Getting the GeometryField and quoted column.
- geo_field = self.query._geo_field(field_name)
- if not geo_field:
- raise TypeError('KML output only available on GeometryFields.')
+ def kml(self, **kwargs):
+ """
+ Returns KML representation of the geometry field in a `kml`
+ attribute on each element of this GeoQuerySet.
+ """
+ s = {'desc' : 'KML',
+ 'procedure_fmt' : '%(geo_col)s,%(precision)s',
+ 'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
+ }
+ return self._spatial_attribute('kml', s, **kwargs)
- geo_col = self.query._field_column(geo_field)
+ def length(self, **kwargs):
+ """
+ Returns the length of the geometry field as a `Distance` object
+ stored in a `length` attribute on each element of this GeoQuerySet.
+ """
+ return self._distance_attribute('length', None, **kwargs)
+
+ def make_line(self, **kwargs):
+ """
+ Creates a linestring from all of the PointField geometries in the
+ this GeoQuerySet and returns it. This is a spatial aggregate
+ method, and thus returns a geometry rather than a GeoQuerySet.
+ """
+ kwargs['geo_field_type'] = PointField
+ kwargs['agg_field'] = GeometryField
+ return self._spatial_aggregate('make_line', **kwargs)
- # Adding the AsKML function call to SELECT part of the SQL.
- return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
+ def mem_size(self, **kwargs):
+ """
+ Returns the memory size (number of bytes) that the geometry field takes
+ in a `mem_size` attribute on each element of this GeoQuerySet.
+ """
+ return self._spatial_attribute('mem_size', {}, **kwargs)
- def transform(self, field_name=None, srid=4326):
+ def num_geom(self, **kwargs):
+ """
+ Returns the number of geometries if the field is a
+ GeometryCollection or Multi* Field in a `num_geom`
+ attribute on each element of this GeoQuerySet; otherwise
+ the sets with None.
+ """
+ return self._spatial_attribute('num_geom', {}, **kwargs)
+
+ def num_points(self, **kwargs):
+ """
+ Returns the number of points in the first linestring in the
+ Geometry field in a `num_points` attribute on each element of
+ this GeoQuerySet; otherwise sets with None.
+ """
+ return self._spatial_attribute('num_points', {}, **kwargs)
+
+ def perimeter(self, **kwargs):
+ """
+ Returns the perimeter of the geometry field as a `Distance` object
+ stored in a `perimeter` attribute on each element of this GeoQuerySet.
+ """
+ return self._distance_attribute('perimeter', None, **kwargs)
+
+ def point_on_surface(self, **kwargs):
+ """
+ Returns a Point geometry guaranteed to lie on the surface of the
+ Geometry field in a `point_on_surface` attribute on each element
+ of this GeoQuerySet; otherwise sets with None.
+ """
+ return self._geom_attribute('point_on_surface', **kwargs)
+
+ def scale(self, x, y, z=0.0, **kwargs):
+ """
+ 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(),
+ }
+ return self._spatial_attribute('scale', s, **kwargs)
+
+ def svg(self, **kwargs):
+ """
+ Returns SVG representation of the geographic field in a `svg`
+ attribute on each element of this GeoQuerySet.
+ """
+ s = {'desc' : 'SVG',
+ 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
+ 'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
+ 'precision' : kwargs.pop('precision', 8)},
+ }
+ return self._spatial_attribute('svg', s, **kwargs)
+
+ def sym_difference(self, geom, **kwargs):
+ """
+ Returns the symmetric difference of the geographic field in a
+ `sym_difference` attribute on each element of this GeoQuerySet.
+ """
+ return self._geomset_attribute('sym_difference', geom, **kwargs)
+
+ def translate(self, x, y, z=0.0, **kwargs):
+ """
+ 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(),
+ }
+ return self._spatial_attribute('translate', s, **kwargs)
+
+ def transform(self, srid=4326, **kwargs):
"""
Transforms the given geometry field to the given SRID. If no SRID is
provided, the transformation will default to using 4326 (WGS84).
"""
- # Getting the geographic field.
- TRANSFORM = SpatialBackend.transform
- if not TRANSFORM:
- raise ImproperlyConfigured('Transform stored procedure not available.')
+ if not isinstance(srid, (int, long)):
+ raise TypeError('An integer SRID must be provided.')
+ field_name = kwargs.get('field_name', None)
+ tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
- # `field_name` is first for backwards compatibility; but we want to
- # be able to take integer srid as first parameter.
- if isinstance(field_name, (int, long)):
- srid = field_name
- field_name = None
-
- # Getting the GeometryField and quoted column.
- geo_field = self.query._geo_field(field_name)
- if not geo_field:
- raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
-
- # Getting the selection SQL for the given geograph
+ # Getting the selection SQL for the given geographic field.
field_col = self._geocol_select(geo_field, field_name)
# Why cascading substitutions? Because spatial backends like
@@ -206,63 +268,334 @@ class GeoQuerySet(QuerySet):
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
- if SpatialBackend.oracle:
- custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
- self.query.ewkt = srid
- else:
- custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
+ custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
+ # TODO: Should we have this as an alias?
+ # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
+ self.query.transformed_srid = srid # So other GeoQuerySet methods
self.query.custom_select[geo_field] = custom_sel
return self._clone()
- def union(self, field_name=None, tolerance=0.0005):
+ def union(self, geom, **kwargs):
+ """
+ Returns the union of the geographic field with the given
+ Geometry in a `union` attribute on each element of this GeoQuerySet.
+ """
+ return self._geomset_attribute('union', geom, **kwargs)
+
+ def unionagg(self, **kwargs):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty. The `tolerance` keyword is for
Oracle backends only.
"""
- # Making sure backend supports the Union stored procedure
- UNION = SpatialBackend.union
- if not UNION:
- raise ImproperlyConfigured('Union stored procedure not available.')
+ kwargs['agg_field'] = GeometryField
+ return self._spatial_aggregate('unionagg', **kwargs)
- # Getting the GeometryField and quoted column.
+ ### Private API -- Abstracted DRY routines. ###
+ def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
+ """
+ Performs set up for executing the spatial function.
+ """
+ # Does the spatial backend support this?
+ func = getattr(SpatialBackend, att, False)
+ if desc is None: desc = att
+ if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
+
+ # Initializing the procedure arguments.
+ procedure_args = {'function' : func}
+
+ # Is there a geographic field in the model to perform this
+ # operation on?
geo_field = self.query._geo_field(field_name)
if not geo_field:
- raise TypeError('Aggregate Union only available on GeometryFields.')
- geo_col = self.query._field_column(geo_field)
+ raise TypeError('%s output only available on GeometryFields.' % func)
+
+ # If the `geo_field_type` keyword was used, then enforce that
+ # type limitation.
+ if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
+ raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__))
- # Replacing the select with a call to the ST_Union stored procedure
- # on the geographic field column.
+ # Setting the procedure args.
+ procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
+
+ return procedure_args, geo_field
+
+ def _spatial_aggregate(self, att, field_name=None,
+ agg_field=None, convert_func=None,
+ geo_field_type=None, tolerance=0.0005):
+ """
+ DRY routine for calling aggregate spatial stored procedures and
+ returning their result to the caller of the function.
+ """
+ # Constructing the setup keyword arguments.
+ setup_kwargs = {'aggregate' : True,
+ 'field_name' : field_name,
+ 'geo_field_type' : geo_field_type,
+ }
+ procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
+
if SpatialBackend.oracle:
- union_sql = '%s' % SpatialBackend.select
- union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
+ procedure_args['tolerance'] = tolerance
+ # Adding in selection SQL for Oracle geometry columns.
+ if agg_field is GeometryField:
+ agg_sql = '%s' % SpatialBackend.select
+ else:
+ agg_sql = '%s'
+ agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
else:
- union_sql = '%s(%s)' % (UNION, geo_col)
+ agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
+
+ # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
+ # specifying the type of the aggregate field.
+ self.query.select = [GeomSQL(agg_sql)]
+ self.query.select_fields = [agg_field]
- # Only want the union SQL to be selected.
- self.query.select = [GeomSQL(union_sql)]
- self.query.select_fields = [GeometryField]
try:
- usql, params = self.query.as_sql()
+ # `asql` => not overriding `sql` module.
+ asql, params = self.query.as_sql()
except sql.datastructures.EmptyResultSet:
- return None
+ return None
- # Getting a cursor, executing the query.
+ # Getting a cursor, executing the query, and extracting the returned
+ # value from the aggregate function.
cursor = connection.cursor()
- cursor.execute(usql, params)
- if SpatialBackend.oracle:
- # On Oracle have to read out WKT from CLOB first.
- clob = cursor.fetchone()[0]
- if clob: u = clob.read()
- else: u = None
+ cursor.execute(asql, params)
+ result = cursor.fetchone()[0]
+
+ # If the `agg_field` is specified as a GeometryField, then autmatically
+ # set up the conversion function.
+ if agg_field is GeometryField and not callable(convert_func):
+ if SpatialBackend.postgis:
+ def convert_geom(hex, geo_field):
+ if hex: return SpatialBackend.Geometry(hex)
+ else: return None
+ elif SpatialBackend.oracle:
+ def convert_geom(clob, geo_field):
+ if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
+ else: return None
+ convert_func = convert_geom
+
+ # Returning the callback function evaluated on the result culled
+ # from the executed cursor.
+ if callable(convert_func):
+ return convert_func(result, geo_field)
+ else:
+ return result
+
+ def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
+ """
+ DRY routine for calling a spatial stored procedure on a geometry column
+ and attaching its output as an attribute of the model.
+
+ Arguments:
+ att:
+ The name of the spatial attribute that holds the spatial
+ SQL function to call.
+
+ settings:
+ Dictonary of internal settings to customize for the spatial procedure.
+
+ Public Keyword Arguments:
+
+ field_name:
+ The name of the geographic field to call the spatial
+ function on. May also be a lookup to a geometry field
+ as part of a foreign key relation.
+
+ model_att:
+ The name of the model attribute to attach the output of
+ the spatial function to.
+ """
+ # Default settings.
+ settings.setdefault('desc', None)
+ settings.setdefault('geom_args', ())
+ settings.setdefault('geom_field', None)
+ settings.setdefault('procedure_args', {})
+ settings.setdefault('procedure_fmt', '%(geo_col)s')
+ settings.setdefault('select_params', [])
+
+ # Performing setup for the spatial column, unless told not to.
+ if settings.get('setup', True):
+ default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
+ for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
else:
- u = cursor.fetchone()[0]
+ geo_field = settings['geo_field']
+
+ # The attribute to attach to the model.
+ if not isinstance(model_att, basestring): model_att = att
+
+ # Special handling for any argument that is a geometry.
+ for name in settings['geom_args']:
+ # Using the field's get_db_prep_lookup() to get any needed
+ # transformation SQL -- we pass in a 'dummy' `contains` lookup.
+ where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
+ # Replacing the procedure format with that of any needed
+ # transformation SQL.
+ old_fmt = '%%(%s)s' % name
+ new_fmt = where[0] % '%%s'
+ settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
+ settings['select_params'].extend(params)
+
+ # Getting the format for the stored procedure.
+ fmt = '%%(function)s(%s)' % settings['procedure_fmt']
- if u: return GEOSGeometry(u)
- else: return None
+ # If the result of this function needs to be converted.
+ if settings.get('select_field', False):
+ sel_fld = settings['select_field']
+ if isinstance(sel_fld, GeomField) and SpatialBackend.select:
+ self.query.custom_select[model_att] = SpatialBackend.select
+ self.query.extra_select_fields[model_att] = sel_fld
+
+ # Finally, setting the extra selection attribute with
+ # the format string expanded with the stored procedure
+ # arguments.
+ return self.extra(select={model_att : fmt % settings['procedure_args']},
+ select_params=settings['select_params'])
+
+ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
+ """
+ DRY routine for GeoQuerySet distance attribute routines.
+ """
+ # Setting up the distance procedure arguments.
+ procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
+
+ # If geodetic defaulting distance attribute to meters (Oracle and
+ # PostGIS spherical distances return meters). Otherwise, use the
+ # units of the geometry field.
+ if geo_field.geodetic:
+ dist_att = 'm'
+ else:
+ dist_att = Distance.unit_attname(geo_field._unit_name)
+
+ # Shortcut booleans for what distance function we're using.
+ distance = func == 'distance'
+ length = func == 'length'
+ perimeter = func == 'perimeter'
+ if not (distance or length or perimeter):
+ raise ValueError('Unknown distance function: %s' % func)
+
+ # The field's get_db_prep_lookup() is used to get any
+ # extra distance parameters. Here we set up the
+ # parameters that will be passed in to field's function.
+ 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
+ # 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):
+ lookup_params.append('spheroid')
+ where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
+
+ # The `geom_args` flag is set to true if a geometry parameter was
+ # passed in.
+ geom_args = bool(geom)
+
+ if SpatialBackend.oracle:
+ if distance:
+ procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
+ elif length or perimeter:
+ procedure_fmt = '%(geo_col)s,%(tolerance)s'
+ procedure_args['tolerance'] = tolerance
+ else:
+ # Getting whether this field is in units of degrees since the field may have
+ # been transformed via the `transform` GeoQuerySet method.
+ if self.query.transformed_srid:
+ u, unit_name, s = get_srid_info(self.query.transformed_srid)
+ geodetic = unit_name in geo_field.geodetic_units
+ else:
+ geodetic = geo_field.geodetic
+
+ if distance:
+ if self.query.transformed_srid:
+ # Setting the `geom_args` flag to false because we want to handle
+ # transformation SQL here, rather than the way done by default
+ # (which will transform to the original SRID of the field rather
+ # than to what was transformed to).
+ geom_args = False
+ procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
+ if geom.srid is None or geom.srid == self.query.transformed_srid:
+ # If the geom parameter srid is None, it is assumed the coordinates
+ # are in the transformed units. A placeholder is used for the
+ # geometry parameter.
+ 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)
+ else:
+ # `transform()` was not used on this GeoQuerySet.
+ procedure_fmt = '%(geo_col)s,%(geom)s'
+
+ if geodetic:
+ # Spherical distance calculation is needed (because the geographic
+ # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
+ # 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.')
+ if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
+ raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
+ # The `function` procedure argument needs to be set differently for
+ # geodetic distance calculations.
+ if spheroid:
+ # Call to distance_spheroid() requires spheroid param as well.
+ procedure_fmt += ',%(spheroid)s'
+ procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
+ else:
+ procedure_args.update({'function' : SpatialBackend.distance_sphere})
+ elif length or perimeter:
+ procedure_fmt = '%(geo_col)s'
+ if geodetic and length:
+ # There's no `length_sphere`
+ procedure_fmt += ',%(spheroid)s'
+ procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
+
+ # Setting up the settings for `_spatial_attribute`.
+ s = {'select_field' : DistanceField(dist_att),
+ 'setup' : False,
+ 'geo_field' : geo_field,
+ 'procedure_args' : procedure_args,
+ 'procedure_fmt' : procedure_fmt,
+ }
+ if geom_args:
+ s['geom_args'] = ('geom',)
+ s['procedure_args']['geom'] = geom
+ elif geom:
+ # The geometry is passed in as a parameter because we handled
+ # transformation conditions in this routine.
+ s['select_params'] = [SpatialBackend.Adaptor(geom)]
+ return self._spatial_attribute(func, s, **kwargs)
+
+ def _geom_attribute(self, func, tolerance=0.05, **kwargs):
+ """
+ DRY routine for setting up a GeoQuerySet method that attaches a
+ Geometry attribute (e.g., `centroid`, `point_on_surface`).
+ """
+ s = {'select_field' : GeomField(),}
+ if SpatialBackend.oracle:
+ s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
+ s['procedure_args'] = {'tolerance' : tolerance}
+ return self._spatial_attribute(func, s, **kwargs)
+
+ def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
+ """
+ DRY routine for setting up a GeoQuerySet method that attaches a
+ Geometry attribute and takes a Geoemtry parameter. This is used
+ for geometry set-like operations (e.g., intersection, difference,
+ union, sym_difference).
+ """
+ s = {'geom_args' : ('geom',),
+ 'select_field' : GeomField(),
+ 'procedure_fmt' : '%(geo_col)s,%(geom)s',
+ 'procedure_args' : {'geom' : geom},
+ }
+ if SpatialBackend.oracle:
+ s['procedure_fmt'] += ',%(tolerance)s'
+ s['procedure_args']['tolerance'] = tolerance
+ return self._spatial_attribute(func, s, **kwargs)
- # Private API utilities, subject to change.
- def _geocol_select(self, geo_field, field_name):
+ def _geocol_select(self, geo_field, field_name, aggregate=False):
"""
Helper routine for constructing the SQL to select the geographic
column. Takes into account if the geographic field is in a
@@ -274,6 +607,8 @@ class GeoQuerySet(QuerySet):
# (e.g., if 'location__point' was given as the field name).
self.query.add_select_related([field_name])
self.query.pre_sql_setup()
+ # Can't non-aggregate and aggregate selections together.
+ if aggregate: self.query.aggregate = True
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
return self.query._field_column(geo_field, rel_table)
else:
diff --git a/django/contrib/gis/db/models/sql/__init__.py b/django/contrib/gis/db/models/sql/__init__.py
index 8e87f6b2a2..4a66b41664 100644
--- a/django/contrib/gis/db/models/sql/__init__.py
+++ b/django/contrib/gis/db/models/sql/__init__.py
@@ -1,2 +1,2 @@
-from django.contrib.gis.db.models.sql.query import GeoQuery
+from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
from django.contrib.gis.db.models.sql.where import GeoWhereNode
diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py
index 9cd691bea9..f1b4de546f 100644
--- a/django/contrib/gis/db/models/sql/query.py
+++ b/django/contrib/gis/db/models/sql/query.py
@@ -6,6 +6,7 @@ from django.db.models.fields.related import ForeignKey
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models.fields import GeometryField
from django.contrib.gis.db.models.sql.where import GeoWhereNode
+from django.contrib.gis.measure import Area, Distance
# Valid GIS query types.
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
@@ -24,14 +25,19 @@ class GeoQuery(sql.Query):
# The following attributes are customized for the GeoQuerySet.
# The GeoWhereNode and SpatialBackend classes contain backend-specific
# routines and functions.
+ self.aggregate = False
self.custom_select = {}
- self.ewkt = None
+ self.transformed_srid = None
+ self.extra_select_fields = {}
def clone(self, *args, **kwargs):
obj = super(GeoQuery, self).clone(*args, **kwargs)
- # Customized selection dictionary and EWKT flag have to be added to obj.
+ # Customized selection dictionary and transformed srid flag have
+ # to also be added to obj.
+ obj.aggregate = self.aggregate
obj.custom_select = self.custom_select.copy()
- obj.ewkt = self.ewkt
+ obj.transformed_srid = self.transformed_srid
+ obj.extra_select_fields = self.extra_select_fields.copy()
return obj
def get_columns(self, with_aliases=False):
@@ -49,7 +55,8 @@ class GeoQuery(sql.Query):
"""
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
- result = ['(%s) AS %s' % (col, qn2(alias)) for alias, col in self.extra_select.iteritems()]
+ result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias))
+ for alias, col in self.extra_select.iteritems()]
aliases = set(self.extra_select.keys())
if with_aliases:
col_aliases = aliases.copy()
@@ -80,17 +87,18 @@ class GeoQuery(sql.Query):
result.extend(cols)
aliases.update(new_aliases)
# This loop customized for GeoQuery.
- for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
- r = self.get_field_select(field, table)
- if with_aliases and col in col_aliases:
- c_alias = 'Col%d' % len(col_aliases)
- result.append('%s AS %s' % (r, c_alias))
- aliases.add(c_alias)
- col_aliases.add(c_alias)
- else:
- result.append(r)
- aliases.add(r)
- col_aliases.add(col)
+ if not self.aggregate:
+ for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
+ r = self.get_field_select(field, table)
+ if with_aliases and col in col_aliases:
+ c_alias = 'Col%d' % len(col_aliases)
+ result.append('%s AS %s' % (r, c_alias))
+ aliases.add(c_alias)
+ col_aliases.add(c_alias)
+ else:
+ result.append(r)
+ aliases.add(r)
+ col_aliases.add(col)
self._select_aliases = aliases
return result
@@ -139,7 +147,51 @@ class GeoQuery(sql.Query):
col_aliases.add(field.column)
return result, aliases
+ def resolve_columns(self, row, fields=()):
+ """
+ This routine is necessary so that distances and geometries returned
+ from extra selection SQL get resolved appropriately into Python
+ objects.
+ """
+ values = []
+ aliases = self.extra_select.keys()
+ index_start = len(aliases)
+ values = [self.convert_values(v, self.extra_select_fields.get(a, None))
+ for v, a in izip(row[:index_start], aliases)]
+ if SpatialBackend.oracle:
+ # This is what happens normally in Oracle's `resolve_columns`.
+ for value, field in izip(row[index_start:], fields):
+ values.append(self.convert_values(value, field))
+ else:
+ values.extend(row[index_start:])
+ return values
+
+ def convert_values(self, value, field):
+ """
+ Using the same routines that Oracle does we can convert our
+ extra selection objects into Geometry and Distance objects.
+ TODO: Laziness.
+ """
+ if SpatialBackend.oracle:
+ # Running through Oracle's first.
+ value = super(GeoQuery, self).convert_values(value, field)
+ 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):
+ value = SpatialBackend.Geometry(value)
+ return value
+
#### Routines unique to GeoQuery ####
+ def get_extra_select_format(self, alias):
+ sel_fmt = '%s'
+ if alias in self.custom_select:
+ sel_fmt = sel_fmt % self.custom_select[alias]
+ return sel_fmt
+
def get_field_select(self, fld, alias=None):
"""
Returns the SELECT SQL string for the given field. Figures out
@@ -173,8 +225,8 @@ class GeoQuery(sql.Query):
# 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.ewkt and SpatialBackend.oracle:
- sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt)
+ if self.transformed_srid and SpatialBackend.oracle:
+ sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
else:
sel_fmt = '%s'
return sel_fmt
@@ -188,8 +240,8 @@ class GeoQuery(sql.Query):
Related model field strings like 'address__point', may also be
used.
- If a GeometryField exists according to the given name
- parameter it will be returned, otherwise returns False.
+ If a GeometryField exists according to the given name parameter
+ it will be returned, otherwise returns False.
"""
if isinstance(name_param, basestring):
# This takes into account the situation where the name is a
@@ -242,3 +294,17 @@ class GeoQuery(sql.Query):
# Otherwise, check by the given field name -- which may be
# a lookup to a _related_ geographic field.
return self._check_geo_field(self.model, field_name)
+
+### Field Classes for `convert_values` ####
+class AreaField(object):
+ def __init__(self, area_att):
+ self.area_att = area_att
+
+class DistanceField(object):
+ def __init__(self, distance_att):
+ self.distance_att = distance_att
+
+# Rather than use GeometryField (which requires a SQL query
+# upon instantiation), use this lighter weight class.
+class GeomField(object):
+ pass
diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py
index 3c37232cfd..d9c75eaec3 100644
--- a/django/contrib/gis/db/models/sql/where.py
+++ b/django/contrib/gis/db/models/sql/where.py
@@ -1,5 +1,5 @@
from django.db.models.sql.where import WhereNode
-from django.contrib.gis.db.backend import get_geo_where_clause, GIS_TERMS
+from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
class GeoWhereNode(WhereNode):
"""
@@ -9,7 +9,7 @@ class GeoWhereNode(WhereNode):
def make_atom(self, child, qn):
table_alias, name, field, lookup_type, value = child
if hasattr(field, '_geom'):
- if lookup_type in GIS_TERMS:
+ if lookup_type in SpatialBackend.gis_terms:
# Getting the geographic where clause; substitution parameters
# will be populated in the GeoFieldSQL object returned by the
# GeometryField.
diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py
index 5af5ab40f6..ea2e57c1ca 100644
--- a/django/contrib/gis/measure.py
+++ b/django/contrib/gis/measure.py
@@ -30,14 +30,60 @@
Distance and Area objects to allow for sensible and convienient calculation
and conversions.
-Author: Robert Coup
+Author: Robert Coup, Justin Bronn
Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
and Geoff Biggs' PhD work on dimensioned units for robotics.
"""
+__all__ = ['A', 'Area', 'D', 'Distance']
from decimal import Decimal
-class Distance(object):
+class MeasureBase(object):
+ def default_units(self, kwargs):
+ """
+ Return the unit value and the the default units specified
+ from the given keyword arguments dictionary.
+ """
+ val = 0.0
+ for unit, value in kwargs.iteritems():
+ if unit in self.UNITS:
+ val += self.UNITS[unit] * value
+ default_unit = unit
+ elif unit in self.ALIAS:
+ u = self.ALIAS[unit]
+ val += self.UNITS[u] * value
+ default_unit = u
+ else:
+ lower = unit.lower()
+ if lower in self.UNITS:
+ val += self.UNITS[lower] * value
+ default_unit = lower
+ elif lower in self.LALIAS:
+ u = self.LALIAS[lower]
+ val += self.UNITS[u] * value
+ default_unit = u
+ else:
+ raise AttributeError('Unknown unit type: %s' % unit)
+ return val, default_unit
+
+ @classmethod
+ def unit_attname(cls, unit_str):
+ """
+ Retrieves the unit attribute name for the given unit string.
+ For example, if the given unit string is 'metre', 'm' would be returned.
+ An exception is raised if an attribute cannot be found.
+ """
+ lower = unit_str.lower()
+ if unit_str in cls.UNITS:
+ return unit_str
+ elif lower in cls.UNITS:
+ return lower
+ elif lower in cls.LALIAS:
+ return cls.LALIAS[lower]
+ else:
+ raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
+
+class Distance(MeasureBase):
UNITS = {
'chain' : 20.1168,
'chain_benoit' : 20.116782,
@@ -53,7 +99,6 @@ class Distance(object):
'fathom' : 1.8288,
'ft': 0.3048,
'german_m' : 1.0000135965,
- 'grad' : 0.0157079632679,
'gold_coast_ft' : 0.304799710181508,
'indian_yd' : 0.914398530744,
'in' : 0.0254,
@@ -92,9 +137,10 @@ class Distance(object):
'British chain (Sears 1922)' : 'british_chain_sears',
'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
'British foot (Sears 1922)' : 'british_ft',
+ 'British foot' : 'british_ft',
'British yard (Sears 1922)' : 'british_yd',
+ 'British yard' : 'british_yd',
"Clarke's Foot" : 'clarke_ft',
- "Clarke's foot" : 'clarke_ft',
"Clarke's link" : 'clarke_link',
'Chain (Benoit)' : 'chain_benoit',
'Chain (Sears)' : 'chain_sears',
@@ -111,33 +157,11 @@ class Distance(object):
'Yard (Indian)' : 'indian_yd',
'Yard (Sears)' : 'sears_yd'
}
- REV_ALIAS = dict((value, key) for key, value in ALIAS.items())
+ LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
# The base unit is in meters.
- self.m = 0.0
- self._default_unit = 'm'
-
- for unit,value in kwargs.items():
- if unit in self.UNITS:
- self.m += self.UNITS[unit] * value
- self._default_unit = unit
- elif unit in self.ALIAS:
- u = self.ALIAS[unit]
- self.m += self.UNITS[u] * value
- self._default_unit = u
- else:
- lower = unit.lower()
- if lower in self.UNITS:
- self.m += self.UNITS[lower] * value
- self._default_unit = lower
- elif lower in self.ALIAS:
- u = self.ALIAS[lower]
- self.m += self.UNITS[u] * value
- self._default_unit = u
- else:
- raise AttributeError('Unknown unit type: %s' % unit)
-
+ self.m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
@@ -216,49 +240,15 @@ class Distance(object):
def __nonzero__(self):
return bool(self.m)
- @classmethod
- def unit_attname(cls, unit_str):
- """
- Retrieves the unit attribute name for the given unit string.
- For example, if the given unit string is 'metre', 'm' would be returned.
- An exception is raised if an attribute cannot be found.
- """
- lower = unit_str.lower()
-
- if unit_str in cls.UNITS:
- return unit_str
- elif lower in cls.UNITS:
- return lower
- elif unit_str in cls.ALIAS:
- return cls.ALIAS[unit_str]
- elif lower in cls.ALIAS:
- return cls.ALIAS[lower]
- else:
- raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
-
-class Area(object):
- # TODO: Add units from above.
- UNITS = {
- 'sq_m': 1.0,
- 'sq_km': 1000000.0,
- 'sq_mi': 2589988.110336,
- 'sq_ft': 0.09290304,
- 'sq_yd': 0.83612736,
- 'sq_nm': 3429904.0,
- }
+class Area(MeasureBase):
+ # Getting the square units values and the alias dictionary.
+ UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
+ ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
+ LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
def __init__(self, default_unit=None, **kwargs):
- self.sq_m = 0.0
- self._default_unit = 'sq_m'
-
- for unit,value in kwargs.items():
- if unit in self.UNITS:
- self.sq_m += self.UNITS[unit] * value
- self._default_unit = unit
- else:
- raise AttributeError('Unknown unit type: ' + unit)
-
- if default_unit:
+ self.sq_m, self._default_unit = self.default_units(kwargs)
+ if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit
def __getattr__(self, name):
@@ -333,7 +323,6 @@ class Area(object):
def __nonzero__(self):
return bool(self.sq_m)
-
# Shortcuts
D = Distance
diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py
index 9d923760a3..8bf8ef4964 100644
--- a/django/contrib/gis/models.py
+++ b/django/contrib/gis/models.py
@@ -209,9 +209,54 @@ class SpatialRefSysMixin(object):
return unicode(self.wkt)
# The SpatialRefSys and GeometryColumns models
+_srid_info = True
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
else:
- pass
+ _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).
+ """
+ from django.db import connection
+ # Getting the spatial reference WKT associated with the SRID from the
+ # `spatial_ref_sys` (or equivalent) spatial database table.
+ #
+ # The following doesn't work: SpatialRefSys.objects.get(srid=srid)
+ # Why? `syncdb` fails to recognize installed geographic models when there's
+ # an ORM query instantiated within a model field.
+ 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)
+ srs_wkt = cur.fetchone()[0]
+ if srs_wkt is None:
+ raise ValueError('Failed to find Spatial Reference System entry corresponding to SRID=%s' % srid)
+
+ # 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
+
diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py
index b3afe9baa7..44465f4f23 100644
--- a/django/contrib/gis/tests/__init__.py
+++ b/django/contrib/gis/tests/__init__.py
@@ -11,7 +11,7 @@ from django.conf import settings
if not settings._target: settings.configure()
# Tests that require use of a spatial database (e.g., creation of models)
-test_models = ['geoapp', 'relatedapp']
+test_models = ['geoapp',]
# Tests that do not require setting up and tearing down a spatial database.
test_suite_names = [
@@ -20,11 +20,14 @@ test_suite_names = [
]
if HAS_GDAL:
if oracle:
- # TODO: There is a problem with the `syncdb` SQL for the LayerMapping
- # tests on Oracle.
- test_models += ['distapp']
- elif postgis:
+ # TODO: There's a problem with `select_related` and GeoQuerySet on
+ # Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
+ # doesn't work so we don't test `relatedapp`.
test_models += ['distapp', 'layermap']
+ elif postgis:
+ test_models += ['distapp', 'layermap', 'relatedapp']
+ elif mysql:
+ test_models += ['relatedapp']
test_suite_names += [
'test_gdal_driver',
diff --git a/django/contrib/gis/tests/distapp/data.py b/django/contrib/gis/tests/distapp/data.py
index 6ccee717c6..fefe313905 100644
--- a/django/contrib/gis/tests/distapp/data.py
+++ b/django/contrib/gis/tests/distapp/data.py
@@ -11,16 +11,23 @@ au_cities = (('Wollongong', 150.902, -34.4245),
('Hillsdale', 151.231341, -33.952685),
)
-stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
- ('West University Place', 943547.922328, 4213623.65345),
- ('Southside Place', 944704.643307, 4212768.87617),
- ('Bellaire', 942595.669129, 4212686.72583),
- ('Pearland', 959674.616506, 4197465.6526),
- ('Galveston', 1008151.16007, 4170027.47655),
- ('Sealy', 874859.286808, 4219186.8641),
- ('San Antonio', 649173.910483, 4176413.27786),
- ('Round Rock', 726846.03695, 4297160.99715),
- ('Saint Hedwig', 677644.649952, 4175467.06744),
+stx_cities = (('Downtown Houston', -95.363151, 29.763374),
+ ('West University Place', -95.448601, 29.713803),
+ ('Southside Place', -95.436920, 29.705777),
+ ('Bellaire', -95.458732, 29.705614),
+ ('Pearland', -95.287303, 29.563568),
+ ('Galveston', -94.797489, 29.301336),
+ ('Sealy', -96.156952, 29.780918),
+ ('San Antonio', -98.493183, 29.424170),
+ ('Saint Hedwig', -98.199820, 29.414197),
)
+# Data from U.S. Census ZCTA cartographic boundary file for Texas (`zt48_d00.shp`).
+stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))'),
+ ('77005', 'POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))'),
+ ('77025', 'POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))'),
+ ('77401', 'POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))'),
+ )
+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)'),
+ )
diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py
index 836b769e61..f23b277fea 100644
--- a/django/contrib/gis/tests/distapp/models.py
+++ b/django/contrib/gis/tests/distapp/models.py
@@ -7,6 +7,13 @@ class SouthTexasCity(models.Model):
objects = models.GeoManager()
def __unicode__(self): return self.name
+class SouthTexasCityFt(models.Model):
+ "Same City model as above, but U.S. survey feet are the units."
+ name = models.CharField(max_length=30)
+ point = models.PointField(srid=2278)
+ objects = models.GeoManager()
+ def __unicode__(self): return self.name
+
class AustraliaCity(models.Model):
"City model for Australia, using WGS84."
name = models.CharField(max_length=30)
@@ -14,7 +21,22 @@ class AustraliaCity(models.Model):
objects = models.GeoManager()
def __unicode__(self): return self.name
-#class County(models.Model):
-# name = models.CharField(max_length=30)
-# mpoly = models.MultiPolygonField(srid=32140)
-# objects = models.GeoManager()
+class CensusZipcode(models.Model):
+ "Model for a few South Texas ZIP codes (in original Census NAD83)."
+ name = models.CharField(max_length=5)
+ poly = models.PolygonField(srid=4269)
+ objects = models.GeoManager()
+
+class SouthTexasZipcode(models.Model):
+ "Model for a few South Texas ZIP codes."
+ name = models.CharField(max_length=5)
+ poly = models.PolygonField(srid=32140)
+ objects = models.GeoManager()
+ def __unicode__(self): return self.name
+
+class Interstate(models.Model):
+ "Geodetic model for U.S. Interstates."
+ name = models.CharField(max_length=10)
+ line = models.LineStringField()
+ 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 d1fe83ebc5..53bcf9d42d 100644
--- a/django/contrib/gis/tests/distapp/tests.py
+++ b/django/contrib/gis/tests/distapp/tests.py
@@ -1,14 +1,15 @@
import os, unittest
from decimal import Decimal
+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
+from django.contrib.gis.tests.utils import oracle, postgis, no_oracle
-from models import SouthTexasCity, AustraliaCity
-from data import au_cities, stx_cities
+from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
+from data import au_cities, interstates, stx_cities, stx_zips
class DistanceTest(unittest.TestCase):
@@ -20,41 +21,64 @@ class DistanceTest(unittest.TestCase):
# Another one for Australia
au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326)
- def get_cities(self, qs):
+ def get_names(self, qs):
cities = [c.name for c in qs]
cities.sort()
return cities
def test01_init(self):
"Initialization of distance models."
-
- def load_cities(city_model, srid, data_tup):
+
+ # 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=srid))
+ c = city_model(name=name, point=Point(x, y, srid=4326))
c.save()
- load_cities(SouthTexasCity, 32140, stx_cities)
- load_cities(AustraliaCity, 4326, au_cities)
+ load_cities(SouthTexasCity, stx_cities)
+ load_cities(SouthTexasCityFt, stx_cities)
+ load_cities(AustraliaCity, au_cities)
- self.assertEqual(10, SouthTexasCity.objects.count())
+ 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)
+ SouthTexasZipcode(name=name, poly=poly).save()
+ CensusZipcode(name=name, poly=poly).save()
+ self.assertEqual(4, SouthTexasZipcode.objects.count())
+ 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())
def test02_dwithin(self):
"Testing the `dwithin` lookup type."
# Distances -- all should be equal (except for the
# degree/meter pair in au_cities, that's somewhat
# approximate).
- tx_dists = [7000, D(km=7), D(mi=4.349)]
+ 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']
+ # Performing distance queries on two projected coordinate systems one
+ # with units in meters and the other in units of U.S. survey feet.
for dist in tx_dists:
- qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
- self.assertEqual(tx_cities, self.get_cities(qs))
+ if isinstance(dist, tuple): dist1, dist2 = dist
+ else: dist1 = dist2 = dist
+ qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1))
+ qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2))
+ for qs in qs1, qs2:
+ self.assertEqual(tx_cities, self.get_names(qs))
+ # Now performing the `dwithin` queries on a geodetic coordinate system.
for dist in au_dists:
if isinstance(dist, D) and not oracle: type_error = True
else: type_error = False
@@ -70,23 +94,29 @@ class DistanceTest(unittest.TestCase):
# Distance objects into a DWithin query using a geodetic field.
self.assertRaises(TypeError, qs.count)
else:
- self.assertEqual(au_cities, self.get_cities(qs))
+ self.assertEqual(au_cities, self.get_names(qs))
- def test03_distance_aggregate(self):
- "Testing the `distance` GeoQuerySet method."
+ 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)
- # Got these from using the raw SQL statement:
- # SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
- distances = [147075.069813, 139630.198056, 140888.552826,
- 138809.684197, 158309.246259, 212183.594374,
- 70870.188967, 165337.758878, 102128.654360,
- 139196.085105]
+ # 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;
+ ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
+ 455411.438904354, 519386.252102563, 696139.009211594,
+ 232513.278304279, 542445.630586414, 456679.155883207]
- # Testing when the field name is explicitly set.
- dist1 = SouthTexasCity.objects.distance('point', lagrange)
+ # Testing using different variations of parameters and using models
+ # with different projected coordinate systems.
+ dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
- dist3 = SouthTexasCity.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
+ dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
+ dist4 = SouthTexasCityFt.objects.distance(lagrange)
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
# for Oracle.
@@ -94,9 +124,15 @@ class DistanceTest(unittest.TestCase):
else: tol = 5
# Ensuring expected distances are returned for each distance queryset.
- for qs in [dist1, dist2, dist3]:
+ for qs in [dist1, dist2, dist3, dist4]:
for i, c in enumerate(qs):
- self.assertAlmostEqual(distances[i], c.distance, tol)
+ self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
+ self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
+
+ def test03b_distance_method(self):
+ "Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
+ if oracle: tol = 2
+ else: tol = 5
# Now testing geodetic distance aggregation.
hillsdale = AustraliaCity.objects.get(name='Hillsdale')
@@ -106,61 +142,153 @@ class DistanceTest(unittest.TestCase):
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
- # Got these distances using the raw SQL statement:
+ # 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));
- geodetic_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
+ spheroid_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
+ # SELECT ST_distance_sphere(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326)) FROM distapp_australiacity WHERE (NOT (id = 11)); st_distance_sphere
+ sphere_distances = [60580.7612632291, 77143.7785056615, 49199.2725132184, 90804.4414289463, 217712.63666124, 709131.691061906, 639825.959074112, 7786.80274606706, 1049200.46122281, 1162619.7297006]
- # Ensuring the expected distances are returned.
- qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
+ # Testing with spheroid distances first.
+ qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point, spheroid=True)
for i, c in enumerate(qs):
- self.assertAlmostEqual(geodetic_distances[i], c.distance, tol)
+ self.assertAlmostEqual(spheroid_distances[i], c.distance.m, tol)
+ if postgis:
+ # PostGIS uses sphere-only distances by default, testing these as well.
+ qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
+ for i, c in enumerate(qs):
+ self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
- def test04_distance_lookups(self):
- "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
- # Only two cities (Houston and Southside Place) should be
- # within 7km of the given point.
- dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
- 7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
- ]
+ @no_oracle # Oracle already handles geographic distance calculation.
+ def test03c_distance_method(self):
+ "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)
+
+ # 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
+ # a non-geographic system.
+ z = SouthTexasZipcode.objects.get(name='77005')
- for dist in dists:
- qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
- for c in qs:
- cities = self.get_cities(qs)
- self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
+ # Reference query:
+ # SELECT ST_Distance(ST_Transform("distapp_censuszipcode"."poly", 32140), ST_GeomFromText('<buffer_wkt>', 32140)) FROM "distapp_censuszipcode";
+ dists_m = [3553.30384972258, 1243.18391525602, 2186.15439472242]
- # Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
- # (thus, Houston and Southside place will be excluded)
- qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
- cities = self.get_cities(qs)
- self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
+ # Having our buffer in the SRID of the transformation and of the field
+ # -- should get the same results. The first buffer has no need for
+ # transformation SQL because it is the same SRID as what was given
+ # to `transform()`. The second buffer will need to be transformed,
+ # however.
+ buf1 = z.poly.centroid.buffer(100)
+ buf2 = buf1.transform(4269, clone=True)
+ for buf in [buf1, buf2]:
+ qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf)
+ self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
+ for i, z in enumerate(qs):
+ self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
- def test05_geodetic_distance(self):
- "Testing distance lookups on geodetic coordinate systems."
+ def test04_distance_lookups(self):
+ "Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
+ # Retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
+ # (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:
+ cities = self.get_names(qs)
+ self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
+
+ # Doing a distance query using Polygons instead of a Point.
+ z = SouthTexasZipcode.objects.get(name='77005')
+ qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=275)))
+ self.assertEqual(['77025', '77401'], self.get_names(qs))
+ # 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))
+ def test05_geodetic_distance_lookups(self):
+ "Testing distance lookups on geodetic coordinate systems."
if not oracle:
# Oracle doesn't have this limitation -- PostGIS only allows geodetic
# distance queries from Points to PointFields.
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
self.assertRaises(TypeError,
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
-
- hobart = AustraliaCity.objects.get(name='Hobart')
-
+ # Too many params (4 in this case) should raise a ValueError.
+ self.assertRaises(ValueError,
+ AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')).count)
+
+ # Not enough params should raise a ValueError.
+ self.assertRaises(ValueError,
+ AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',)).count)
+
# Getting all cities w/in 550 miles of Hobart.
+ hobart = AustraliaCity.objects.get(name='Hobart')
qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
- cities = self.get_cities(qs)
+ cities = self.get_names(qs)
self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
# Cities that are either really close or really far from Wollongong --
# and using different units of distance.
wollongong = AustraliaCity.objects.get(name='Wollongong')
- gq1 = GeoQ(point__distance_lte=(wollongong.point, D(yd=19500))) # Yards (~17km)
- gq2 = GeoQ(point__distance_gte=(wollongong.point, D(nm=400))) # Nautical Miles
- qs = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
- cities = self.get_cities(qs)
- self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
+ 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))
+ 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).
+ if postgis:
+ gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
+ gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
+ qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4)
+ querysets = [qs1, qs2]
+ else:
+ querysets = [qs1]
+
+ for qs in querysets:
+ cities = self.get_names(qs)
+ self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
+
+ def test06_area(self):
+ "Testing the `area` GeoQuerySet method."
+ # Reference queries:
+ # SELECT ST_Area(poly) FROM distapp_southtexaszipcode;
+ area_sq_m = [5437908.90234375, 10183031.4389648, 11254471.0073242, 9881708.91772461]
+ # Tolerance has to be lower for Oracle and differences
+ # with GEOS 3.0.0RC4
+ tol = 2
+ for i, z in enumerate(SouthTexasZipcode.objects.area()):
+ self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol)
+
+ def test07_length(self):
+ "Testing the `length` GeoQuerySet method."
+ # Reference query (should use `length_spheroid`).
+ # SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]');
+ len_m = 473504.769553813
+ qs = Interstate.objects.length()
+ if oracle: tol = 2
+ else: tol = 7
+ self.assertAlmostEqual(len_m, qs[0].length.m, tol)
+
+ def test08_perimeter(self):
+ "Testing the `perimeter` GeoQuerySet method."
+ # Reference query:
+ # SELECT ST_Perimeter(distapp_southtexaszipcode.poly) FROM distapp_southtexaszipcode;
+ perim_m = [18404.3550889361, 15627.2108551001, 20632.5588368978, 17094.5996143697]
+ if oracle: tol = 2
+ else: tol = 7
+ for i, z in enumerate(SouthTexasZipcode.objects.perimeter()):
+ self.assertAlmostEqual(perim_m[i], z.perimeter.m, tol)
+ # Running on points; should return 0.
+ for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')):
+ self.assertEqual(0, c.perim.m)
def suite():
s = unittest.TestSuite()
diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py
index b06ec2130b..e2fd392f3a 100644
--- a/django/contrib/gis/tests/geoapp/tests.py
+++ b/django/contrib/gis/tests/geoapp/tests.py
@@ -28,14 +28,10 @@ class GeoModelTest(unittest.TestCase):
def get_file(wkt_file):
return os.path.join(data_dir, wkt_file)
- co = State(name='Colorado', poly=fromfile(get_file('co.wkt')))
- co.save()
- ks = State(name='Kansas', poly=fromfile(get_file('ks.wkt')))
- ks.save()
- tx = Country(name='Texas', mpoly=fromfile(get_file('tx.wkt')))
- tx.save()
- nz = Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt')))
- nz.save()
+ State(name='Colorado', poly=fromfile(get_file('co.wkt'))).save()
+ State(name='Kansas', poly=fromfile(get_file('ks.wkt'))).save()
+ Country(name='Texas', mpoly=fromfile(get_file('tx.wkt'))).save()
+ Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt'))).save()
# Ensuring that data was loaded from initial SQL.
self.assertEqual(2, Country.objects.count())
@@ -49,7 +45,7 @@ class GeoModelTest(unittest.TestCase):
def test02_proxy(self):
"Testing Lazy-Geometry support (using the GeometryProxy)."
if DISABLE: return
- #### Testing on a Point
+ ## Testing on a Point
pnt = Point(0, 0)
nullcity = City(name='NullCity', point=pnt)
nullcity.save()
@@ -86,7 +82,7 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point)
nullcity.delete()
- #### Testing on a Polygon
+ ## Testing on a Polygon
shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0))
inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
@@ -136,7 +132,7 @@ class GeoModelTest(unittest.TestCase):
ref_kml = ref_kml2
# Ensuring the KML is as expected.
- ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
+ ptown1 = City.objects.kml(field_name='point', precision=9).get(name='Pueblo')
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
for ptown in [ptown1, ptown2]:
self.assertEqual(ref_kml, ptown.kml)
@@ -147,8 +143,8 @@ class GeoModelTest(unittest.TestCase):
# Should throw a TypeError when tyring to obtain GML from a
# non-geometry field.
qs = City.objects.all()
- self.assertRaises(TypeError, qs.gml, 'name')
- ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
+ self.assertRaises(TypeError, qs.gml, field_name='name')
+ ptown1 = City.objects.gml(field_name='point', precision=9).get(name='Pueblo')
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
if oracle:
@@ -172,12 +168,12 @@ class GeoModelTest(unittest.TestCase):
# Asserting the result of the transform operation with the values in
# the pre-transformed points. Oracle does not have the 3084 SRID.
if not oracle:
- h = City.objects.transform('point', srid=htown.srid).get(name='Houston')
+ h = City.objects.transform(htown.srid).get(name='Houston')
self.assertEqual(3084, h.point.srid)
self.assertAlmostEqual(htown.x, h.point.x, prec)
self.assertAlmostEqual(htown.y, h.point.y, prec)
- p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
+ p1 = City.objects.transform(ptown.srid, field_name='point').get(name='Pueblo')
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
for p in [p1, p2]:
self.assertEqual(2774, p.point.srid)
@@ -186,7 +182,7 @@ class GeoModelTest(unittest.TestCase):
@no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
def test05_extent(self):
- "Testing the extent() GeoManager method."
+ "Testing the `extent` GeoQuerySet method."
if DISABLE: return
# Reference query:
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
@@ -199,6 +195,17 @@ class GeoModelTest(unittest.TestCase):
for val, exp in zip(extent, expected):
self.assertAlmostEqual(exp, val, 8)
+ @no_oracle
+ def test06_make_line(self):
+ "Testing the `make_line` GeoQuerySet method."
+ # Ensuring that a `TypeError` is raised on models without PointFields.
+ self.assertRaises(TypeError, State.objects.make_line)
+ self.assertRaises(TypeError, Country.objects.make_line)
+ # Reference query:
+ # SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city;
+ self.assertEqual(GEOSGeometry('LINESTRING(-95.363151 29.763374,-96.801611 32.782057,-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)', srid=4326),
+ City.objects.make_line())
+
def test09_disjoint(self):
"Testing the `disjoint` lookup type."
if DISABLE: return
@@ -317,8 +324,7 @@ class GeoModelTest(unittest.TestCase):
# Saving another commonwealth w/a NULL geometry.
if not oracle:
# TODO: Fix saving w/NULL geometry on Oracle.
- nmi = State(name='Northern Mariana Islands', poly=None)
- nmi.save()
+ State(name='Northern Mariana Islands', poly=None).save()
@no_oracle # No specific `left` or `right` operators in Oracle.
def test13_left_right(self):
@@ -416,34 +422,36 @@ class GeoModelTest(unittest.TestCase):
c = City()
self.assertEqual(c.point, None)
- def test17_union(self):
- "Testing the union() GeoManager method."
+ def test17_unionagg(self):
+ "Testing the `unionagg` (aggregate union) GeoManager method."
if DISABLE: return
tx = Country.objects.get(name='Texas').mpoly
- # Houston, Dallas, San Antonio
- union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
+ # Houston, Dallas, San Antonio -- Oracle has different order.
+ union1 = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
+ union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374,-98.493183 29.424170)')
qs = City.objects.filter(point__within=tx)
- self.assertRaises(TypeError, qs.union, 'name')
- u1 = qs.union('point')
- u2 = qs.union()
- self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
- self.assertEqual(True, union.equals_exact(u2, 10))
+ self.assertRaises(TypeError, qs.unionagg, 'name')
+ u1 = qs.unionagg(field_name='point')
+ u2 = qs.unionagg()
+ tol = 0.00001
+ if SpatialBackend.oracle:
+ union = union2
+ else:
+ 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.union('point'))
+ self.assertEqual(None, qs.unionagg(field_name='point'))
def test18_geometryfield(self):
"Testing GeometryField."
if DISABLE: return
- f1 = Feature(name='Point', geom=Point(1, 1))
- f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
- f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
- f4 = Feature(name='GeometryCollection',
- geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
- Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))))
- f1.save()
- f2.save()
- f3.save()
- f4.save()
+ Feature(name='Point', geom=Point(1, 1)).save()
+ Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save()
+ Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save()
+ Feature(name='GeometryCollection',
+ geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
+ Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))).save()
f_1 = Feature.objects.get(name='Point')
self.assertEqual(True, isinstance(f_1.geom, Point))
@@ -458,6 +466,82 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
self.assertEqual(f_3.geom, f_4.geom[2])
+ def test19_centroid(self):
+ "Testing the `centroid` GeoQuerySet method."
+ qs = State.objects.exclude(poly__isnull=True).centroid()
+ if oracle: tol = 0.1
+ else: tol = 0.000000001
+ for s in qs:
+ self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
+
+ def test20_pointonsurface(self):
+ "Testing the `point_on_surface` GeoQuerySet method."
+ # Reference values.
+ if SpatialBackend.oracle:
+ # SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY;
+ ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
+ 'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326),
+ }
+ elif SpatialBackend.postgis:
+ # 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)
+
+ @no_oracle
+ def test21_scale(self):
+ "Testing the `scale` GeoQuerySet method."
+ xfac, yfac = 2, 3
+ qs = Country.objects.scale(xfac, yfac, model_att='scaled')
+ for c in qs:
+ 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])
+
+ @no_oracle
+ def test22_translate(self):
+ "Testing the `translate` GeoQuerySet method."
+ xfac, yfac = 5, -23
+ qs = Country.objects.translate(xfac, yfac, model_att='translated')
+ for c in qs:
+ 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])
+
+ def test23_numgeom(self):
+ "Testing the `num_geom` GeoQuerySet method."
+ # Both 'countries' only have two geometries.
+ for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom)
+ for c in City.objects.filter(point__isnull=False).num_geom():
+ # Oracle will return 1 for the number of geometries on non-collections,
+ # whereas PostGIS will return None.
+ if postgis: self.assertEqual(None, c.num_geom)
+ else: self.assertEqual(1, c.num_geom)
+
+ def test24_numpoints(self):
+ "Testing the `num_points` GeoQuerySet method."
+ for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points)
+ if postgis:
+ # 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."
+ 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)
+
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(GeoModelTest))
diff --git a/django/contrib/gis/tests/geoapp/tests_mysql.py b/django/contrib/gis/tests/geoapp/tests_mysql.py
index 89496e81b9..4e7c411f52 100644
--- a/django/contrib/gis/tests/geoapp/tests_mysql.py
+++ b/django/contrib/gis/tests/geoapp/tests_mysql.py
@@ -169,9 +169,9 @@ class GeoModelTest(unittest.TestCase):
def test07_mysql_limitations(self):
"Testing that union(), kml(), gml() raise exceptions."
- self.assertRaises(ImproperlyConfigured, City.objects.union, 'point')
- self.assertRaises(ImproperlyConfigured, State.objects.all().kml, 'poly')
- self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, 'mpoly')
+ self.assertRaises(ImproperlyConfigured, City.objects.union, Point(5, 23), field_name='point')
+ self.assertRaises(ImproperlyConfigured, State.objects.all().kml, field_name='poly')
+ self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, field_name='mpoly')
def suite():
s = unittest.TestSuite()
diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py
index fe7604f980..5dbd528030 100644
--- a/django/contrib/gis/tests/layermap/models.py
+++ b/django/contrib/gis/tests/layermap/models.py
@@ -19,7 +19,7 @@ class City(models.Model):
name = models.CharField(max_length=25)
population = models.IntegerField()
density = models.DecimalField(max_digits=7, decimal_places=1)
- date = models.DateField()
+ dt = models.DateField()
point = models.PointField()
objects = models.GeoManager()
@@ -42,7 +42,7 @@ cofeat_mapping = {'name' : 'Name',
city_mapping = {'name' : 'Name',
'population' : 'Population',
'density' : 'Density',
- 'date' : 'Created',
+ 'dt' : 'Created',
'point' : 'POINT',
}
diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py
index 278c1a2998..740ab8647e 100644
--- a/django/contrib/gis/tests/layermap/tests.py
+++ b/django/contrib/gis/tests/layermap/tests.py
@@ -69,7 +69,7 @@ class LayerMapTest(unittest.TestCase):
city = City.objects.get(name=feat['Name'].value)
self.assertEqual(feat['Population'].value, city.population)
self.assertEqual(Decimal(str(feat['Density'])), city.density)
- self.assertEqual(feat['Created'].value, city.date)
+ self.assertEqual(feat['Created'].value, city.dt)
# Comparing the geometries.
pnt1, pnt2 = feat.geom, city.point
diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py
index a49b66c0dd..5fe63cbc8e 100644
--- a/django/contrib/gis/tests/relatedapp/tests.py
+++ b/django/contrib/gis/tests/relatedapp/tests.py
@@ -34,7 +34,7 @@ class RelatedGeoModelTest(unittest.TestCase):
@no_mysql
def test03_transform_related(self):
- "Testing the `transform` GeoManager method on related geographic models."
+ "Testing the `transform` GeoQuerySet method on related geographic models."
# All the transformations are to state plane coordinate systems using
# US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot).
if postgis:
@@ -47,6 +47,7 @@ class RelatedGeoModelTest(unittest.TestCase):
def check_pnt(ref, pnt):
self.assertAlmostEqual(ref.x, pnt.x, tol)
self.assertAlmostEqual(ref.y, pnt.y, tol)
+ self.assertEqual(ref.srid, pnt.srid)
# Turning on debug so we can manually verify the number of SQL queries issued.
# DISABLED: the number of queries count testing mechanism is way too brittle.
@@ -62,13 +63,35 @@ class RelatedGeoModelTest(unittest.TestCase):
for name, srid, wkt in transformed:
# Doing this implicitly sets `select_related` select the location.
- qs = list(City.objects.filter(name=name).transform('location__point', srid))
- check_pnt(GEOSGeometry(wkt), qs[0].location.point)
+ qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
+ check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
#settings.DEBUG= dbg
# Verifying the number of issued SQL queries.
#self.assertEqual(nqueries, len(connection.queries))
+ @no_mysql
+ def test04_related_aggregate(self):
+ "Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models."
+ if postgis:
+ # One for all locations, one that excludes Roswell.
+ all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
+ txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633)
+ e1 = City.objects.extent(field_name='location__point')
+ e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point')
+ for ref, e in [(all_extent, e1), (txpa_extent, e2)]:
+ for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val)
+
+ # The second union is for a query that has something in the WHERE clause.
+ ref_u1 = GEOSGeometry('MULTIPOINT(-104.528056 33.387222,-97.516111 33.058333,-79.460734 40.18476)', 4326)
+ ref_u2 = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-79.460734 40.18476)', 4326)
+ u1 = City.objects.unionagg(field_name='location__point')
+ u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point')
+ self.assertEqual(ref_u1, u1)
+ self.assertEqual(ref_u2, u2)
+
+ # TODO: Related tests for KML, GML, and distance lookups.
+
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(RelatedGeoModelTest))