Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit 4ec80c4333b618fc1ef2a02c7b8ca8719792f25f 1 parent eb30cad
@jbronn jbronn authored
Showing with 1,366 additions and 543 deletions.
  1. +3 −84 django/contrib/gis/db/backend/__init__.py
  2. +29 −0 django/contrib/gis/db/backend/base.py
  3. +12 −0 django/contrib/gis/db/backend/mysql/__init__.py
  4. +31 −0 django/contrib/gis/db/backend/oracle/__init__.py
  5. +5 −0 django/contrib/gis/db/backend/oracle/adaptor.py
  6. +12 −1 django/contrib/gis/db/backend/oracle/query.py
  7. +42 −0 django/contrib/gis/db/backend/postgis/__init__.py
  8. +0 −1  django/contrib/gis/db/backend/postgis/field.py
  9. +52 −14 django/contrib/gis/db/backend/postgis/query.py
  10. +29 −45 django/contrib/gis/db/models/fields/__init__.py
  11. +51 −0 django/contrib/gis/db/models/manager.py
  12. +4 −4 django/contrib/gis/db/models/proxy.py
  13. +508 −173 django/contrib/gis/db/models/query.py
  14. +1 −1  django/contrib/gis/db/models/sql/__init__.py
  15. +85 −19 django/contrib/gis/db/models/sql/query.py
  16. +2 −2 django/contrib/gis/db/models/sql/where.py
  17. +59 −70 django/contrib/gis/measure.py
  18. +46 −1 django/contrib/gis/models.py
  19. +8 −5 django/contrib/gis/tests/__init__.py
  20. +17 −10 django/contrib/gis/tests/distapp/data.py
  21. +26 −4 django/contrib/gis/tests/distapp/models.py
  22. +190 −62 django/contrib/gis/tests/distapp/tests.py
  23. +122 −38 django/contrib/gis/tests/geoapp/tests.py
  24. +3 −3 django/contrib/gis/tests/geoapp/tests_mysql.py
  25. +2 −2 django/contrib/gis/tests/layermap/models.py
  26. +1 −1  django/contrib/gis/tests/layermap/tests.py
  27. +26 −3 django/contrib/gis/tests/relatedapp/tests.py
View
87 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
View
29 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
+
+
+
+
View
12 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)
View
31 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,
+ )
View
5 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
View
13 django/contrib/gis/db/backend/oracle/query.py
@@ -15,10 +15,21 @@
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.
View
42 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,
+ )
View
1  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
View
66 django/contrib/gis/db/backend/postgis/query.py
@@ -21,7 +21,7 @@
# 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 @@
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 @@ def __init__(self, operator):
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 @@ def __init__(self, pattern):
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 @@ def get_dist_ops(operator):
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:
View
74 django/contrib/gis/db/models/fields/__init__.py
@@ -3,15 +3,12 @@
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 @@ def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
# 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 @@ def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
### 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 @@ def get_distance(self, dist, lookup_type):
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 @@ def get_geometry(self, value):
# 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 @@ def get_srid(self, geom):
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 @@ def get_db_prep_lookup(self, lookup_type, value):
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 @@ def get_db_prep_lookup(self, lookup_type, value):
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 @@ def get_db_prep_lookup(self, lookup_type, value):
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
View
51 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)
View
8 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 specified during initialization and the HEXEWKB value of the field.
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
View
681 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 @@ def __init__(self, model=None, query=None):
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]
-
- 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 gml(self, field_name=None, precision=8, version=2):
+ 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)
+
+ 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, 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)
- # Adding the AsKML function call to SELECT part of the SQL.
- return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
+ 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)
+
+ 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 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 transform(self, field_name=None, srid=4326):
+ 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.')
-
- # `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
+ 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)
- # 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 @@ def transform(self, field_name=None, srid=4326):
# 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__))
+
+ # Setting the procedure args.
+ procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
+
+ return procedure_args, geo_field
- # Replacing the select with a call to the ST_Union stored procedure
- # on the geographic field column.
+ 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:
- u = cursor.fetchone()[0]
+ 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:
+ 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 @@ def _geocol_select(self, geo_field, field_name):
# (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:
View
2  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
View
104 django/contrib/gis/db/models/sql/query.py
@@ -6,6 +6,7 @@
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 @@ def __init__(self, model, conn):
# 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 @@ def get_columns(self, with_aliases=False):
"""
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 @@ def get_columns(self, with_aliases=False):
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 @@ def get_default_columns(self, with_aliases=False, col_aliases=None):
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 @@ def get_select_format(self, fld):
# 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 @@ def _check_geo_field(self, model, name_param):
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 @@ def _geo_field(self, field_name=None):
# 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
View
4 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.
View
129 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 @@ def __idiv__(self, other):
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 @@ def __idiv__(self, other):
def __nonzero__(self):
return bool(self.sq_m)
-
# Shortcuts
D = Distance
View
47 django/contrib/gis/models.py
@@ -209,9 +209,54 @@ def __unicode__(self):
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
+
View
13 django/contrib/gis/tests/__init__.py
@@ -11,7 +11,7 @@
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 @@
]
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',
View
27 django/contrib/gis/tests/distapp/data.py
@@ -11,16 +11,23 @@
('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)'),
+ )
View
30 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
View
252 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