--- a/web/lib/django/contrib/gis/db/models/query.py Wed May 19 17:43:59 2010 +0200
+++ b/web/lib/django/contrib/gis/db/models/query.py Tue May 25 02:43:45 2010 +0200
@@ -1,20 +1,19 @@
-from django.core.exceptions import ImproperlyConfigured
-from django.db import connection
+from django.db import connections
from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet
-from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.db.models import aggregates
-from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField
+from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField, LineStringField
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
+from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Area, Distance
class GeoQuerySet(QuerySet):
"The Geographic QuerySet."
### Methods overloaded from QuerySet ###
- def __init__(self, model=None, query=None):
- super(GeoQuerySet, self).__init__(model=model, query=query)
- self.query = query or GeoQuery(self.model, connection)
+ def __init__(self, model=None, query=None, using=None):
+ super(GeoQuerySet, self).__init__(model=model, query=query, using=using)
+ self.query = query or GeoQuery(self.model)
def values(self, *fields):
return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields)
@@ -42,14 +41,16 @@
'geo_field' : geo_field,
'setup' : False,
}
- if SpatialBackend.oracle:
+ connection = connections[self.db]
+ backend = connection.ops
+ if backend.oracle:
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 or SpatialBackend.spatialite:
- if not geo_field.geodetic:
+ elif backend.postgis or backend.spatialite:
+ if not geo_field.geodetic(connection):
# Getting the area units of the geographic field.
- s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name))
+ s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection)))
else:
# TODO: Do we want to support raw number areas for geodetic fields?
raise Exception('Area on geodetic coordinate systems not supported.')
@@ -110,6 +111,23 @@
"""
return self._spatial_aggregate(aggregates.Extent, **kwargs)
+ def extent3d(self, **kwargs):
+ """
+ Returns the aggregate extent, in 3D, of the features in the
+ GeoQuerySet. It is returned as a 6-tuple, comprising:
+ (xmin, ymin, zmin, xmax, ymax, zmax).
+ """
+ return self._spatial_aggregate(aggregates.Extent3D, **kwargs)
+
+ def force_rhr(self, **kwargs):
+ """
+ Returns a modified version of the Polygon/MultiPolygon in which
+ all of the vertices follow the Right-Hand-Rule. By default,
+ this is attached as the `force_rhr` attribute on each element
+ of the GeoQuerySet.
+ """
+ return self._geom_attribute('force_rhr', **kwargs)
+
def geojson(self, precision=8, crs=False, bbox=False, **kwargs):
"""
Returns a GeoJSON representation of the geomtry field in a `geojson`
@@ -119,16 +137,16 @@
the coordinate reference system and the bounding box to be included
in the GeoJSON representation of the geometry.
"""
- if not SpatialBackend.postgis or not SpatialBackend.geojson:
+ backend = connections[self.db].ops
+ if not backend.geojson:
raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.')
-
+
if not isinstance(precision, (int, long)):
raise TypeError('Precision keyword must be set with an integer.')
-
+
# Setting the options flag -- which depends on which version of
# PostGIS we're using.
- major, minor1, minor2 = SpatialBackend.version
- if major >=1 and (minor1 >= 4):
+ if backend.spatial_version >= (1, 4, 0):
options = 0
if crs and bbox: options = 3
elif bbox: options = 1
@@ -138,23 +156,37 @@
if crs and bbox: options = 3
elif crs: options = 1
elif bbox: options = 2
- s = {'desc' : 'GeoJSON',
+ s = {'desc' : 'GeoJSON',
'procedure_args' : {'precision' : precision, 'options' : options},
'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s',
}
return self._spatial_attribute('geojson', s, **kwargs)
+ def geohash(self, precision=20, **kwargs):
+ """
+ Returns a GeoHash representation of the given field in a `geohash`
+ attribute on each element of the GeoQuerySet.
+
+ The `precision` keyword may be used to custom the number of
+ _characters_ used in the output GeoHash, the default is 20.
+ """
+ s = {'desc' : 'GeoHash',
+ 'procedure_args': {'precision': precision},
+ 'procedure_fmt': '%(geo_col)s,%(precision)s',
+ }
+ return self._spatial_attribute('geohash', s, **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.
"""
+ backend = connections[self.db].ops
s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
- if SpatialBackend.postgis:
+ if backend.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)):
+ if backend.spatial_version > (1, 3, 1):
procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
else:
procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
@@ -235,12 +267,23 @@
"""
return self._geom_attribute('point_on_surface', **kwargs)
+ def reverse_geom(self, **kwargs):
+ """
+ Reverses the coordinate order of the geometry, and attaches as a
+ `reverse` attribute on each element of this GeoQuerySet.
+ """
+ s = {'select_field' : GeomField(),}
+ kwargs.setdefault('model_att', 'reverse_geom')
+ if connections[self.db].ops.oracle:
+ s['geo_field_type'] = LineStringField
+ return self._spatial_attribute('reverse', s, **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.
"""
- if SpatialBackend.spatialite:
+ if connections[self.db].ops.spatialite:
if z != 0.0:
raise NotImplementedError('SpatiaLite does not support 3D scaling.')
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
@@ -300,10 +343,10 @@
terms of relative moves (rather than absolute).
`precision` => May be used to set the maximum number of decimal
- digits used in output (defaults to 8).
+ digits used in output (defaults to 8).
"""
relative = int(bool(relative))
- if not isinstance(precision, (int, long)):
+ if not isinstance(precision, (int, long)):
raise TypeError('SVG precision keyword argument must be an integer.')
s = {'desc' : 'SVG',
'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
@@ -325,7 +368,7 @@
Translates the geometry to a new location using the given numeric
parameters as offsets.
"""
- if SpatialBackend.spatialite:
+ if connections[self.db].ops.spatialite:
if z != 0.0:
raise NotImplementedError('SpatiaLite does not support 3D translation.')
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
@@ -360,7 +403,7 @@
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
- custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
+ custom_sel = '%s(%s, %s)' % (connections[self.db].ops.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
@@ -388,9 +431,13 @@
Performs set up for executing the spatial function.
"""
# Does the spatial backend support this?
- func = getattr(SpatialBackend, att, False)
+ connection = connections[self.db]
+ func = getattr(connection.ops, att, False)
if desc is None: desc = att
- if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
+ if not func:
+ raise NotImplementedError('%s stored procedure not available on '
+ 'the %s backend.' %
+ (desc, connection.ops.name))
# Initializing the procedure arguments.
procedure_args = {'function' : func}
@@ -434,7 +481,7 @@
# Adding any keyword parameters for the Aggregate object. Oracle backends
# in particular need an additional `tolerance` parameter.
agg_kwargs = {}
- if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance
+ if connections[self.db].ops.oracle: agg_kwargs['tolerance'] = tolerance
# Calling the QuerySet.aggregate, and returning only the value of the aggregate.
return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg']
@@ -471,9 +518,13 @@
settings.setdefault('procedure_fmt', '%(geo_col)s')
settings.setdefault('select_params', [])
+ connection = connections[self.db]
+ backend = connection.ops
+
# 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)
+ default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name,
+ geo_field_type=settings.get('geo_field_type', None))
for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
else:
geo_field = settings['geo_field']
@@ -483,13 +534,16 @@
# 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])
+ # Using the field's get_placeholder() routine to get any needed
+ # transformation SQL.
+ geom = geo_field.get_prep_value(settings['procedure_args'][name])
+ params = geo_field.get_db_prep_lookup('contains', geom, connection=connection)
+ geom_placeholder = geo_field.get_placeholder(geom, connection)
+
# Replacing the procedure format with that of any needed
# transformation SQL.
old_fmt = '%%(%s)s' % name
- new_fmt = where[0] % '%%s'
+ new_fmt = geom_placeholder % '%%s'
settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
settings['select_params'].extend(params)
@@ -499,8 +553,10 @@
# 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
+ if isinstance(sel_fld, GeomField) and backend.select:
+ self.query.custom_select[model_att] = backend.select
+ if connection.ops.oracle:
+ sel_fld.empty_strings_allowed = False
self.query.extra_select_fields[model_att] = sel_fld
# Finally, setting the extra selection attribute with
@@ -519,36 +575,47 @@
# 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:
+ connection = connections[self.db]
+ geodetic = geo_field.geodetic(connection)
+ geography = geo_field.geography
+
+ if geodetic:
dist_att = 'm'
else:
- dist_att = Distance.unit_attname(geo_field.units_name)
+ dist_att = Distance.unit_attname(geo_field.units_name(connection))
- # Shortcut booleans for what distance function we're using.
+ # Shortcut booleans for what distance function we're using and
+ # whether the geometry field is 3D.
distance = func == 'distance'
length = func == 'length'
perimeter = func == 'perimeter'
if not (distance or length or perimeter):
raise ValueError('Unknown distance function: %s' % func)
+ geom_3d = geo_field.dim == 3
# 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]
+ # Getting the spatial backend operations.
+ backend = connection.ops
+
# If the spheroid calculation is desired, either by the `spheroid`
# keyword or when calculating the length of geodetic field, make
# sure the 'spheroid' distance setting string is passed in so we
# get the correct spatial stored procedure.
- if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
+ if spheroid or (backend.postgis and geodetic and
+ (not geography) and length):
lookup_params.append('spheroid')
- where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
+ lookup_params = geo_field.get_prep_value(lookup_params)
+ params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection)
# The `geom_args` flag is set to true if a geometry parameter was
# passed in.
geom_args = bool(geom)
- if SpatialBackend.oracle:
+ if backend.oracle:
if distance:
procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
elif length or perimeter:
@@ -558,12 +625,10 @@
# 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)
+ u, unit_name, s = get_srid_info(self.query.transformed_srid, connection)
geodetic = unit_name in geo_field.geodetic_units
- else:
- geodetic = geo_field.geodetic
- if SpatialBackend.spatialite and geodetic:
+ if backend.spatialite and geodetic:
raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.')
if distance:
@@ -573,14 +638,14 @@
# (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)
+ procedure_fmt = '%s(%%(geo_col)s, %s)' % (backend.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. `GeomFromText` constructor is also needed
# to wrap geom placeholder for SpatiaLite.
- if SpatialBackend.spatialite:
- procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.from_text, self.query.transformed_srid)
+ if backend.spatialite:
+ procedure_fmt += ', %s(%%%%s, %s)' % (backend.from_text, self.query.transformed_srid)
else:
procedure_fmt += ', %%s'
else:
@@ -588,38 +653,46 @@
# so wrapping the geometry placeholder in transformation SQL.
# SpatiaLite also needs geometry placeholder wrapped in `GeomFromText`
# constructor.
- if SpatialBackend.spatialite:
- procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (SpatialBackend.transform, SpatialBackend.from_text,
+ if backend.spatialite:
+ procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text,
geom.srid, self.query.transformed_srid)
else:
- procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
+ procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid)
else:
# `transform()` was not used on this GeoQuerySet.
procedure_fmt = '%(geo_col)s,%(geom)s'
- if geodetic:
+ if not geography and 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 ValueError('Spherical distance calculation only supported on PointFields.')
- if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
- raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
+ if not backend.geography:
+ if not isinstance(geo_field, PointField):
+ raise ValueError('Spherical distance calculation only supported on PointFields.')
+ if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
+ raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
# The `function` procedure argument needs to be set differently for
# geodetic distance calculations.
if spheroid:
# Call to distance_spheroid() requires spheroid param as well.
- procedure_fmt += ',%(spheroid)s'
- procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
+ procedure_fmt += ",'%(spheroid)s'"
+ procedure_args.update({'function' : backend.distance_spheroid, 'spheroid' : params[1]})
else:
- procedure_args.update({'function' : SpatialBackend.distance_sphere})
+ procedure_args.update({'function' : backend.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]})
+ if not geography and geodetic and length:
+ # There's no `length_sphere`, and `length_spheroid` also
+ # works on 3D geometries.
+ procedure_fmt += ",'%(spheroid)s'"
+ procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]})
+ elif geom_3d and backend.postgis:
+ # Use 3D variants of perimeter and length routines on PostGIS.
+ if perimeter:
+ procedure_args.update({'function' : backend.perimeter3d})
+ elif length:
+ procedure_args.update({'function' : backend.length3d})
# Setting up the settings for `_spatial_attribute`.
s = {'select_field' : DistanceField(dist_att),
@@ -634,7 +707,7 @@
elif geom:
# The geometry is passed in as a parameter because we handled
# transformation conditions in this routine.
- s['select_params'] = [SpatialBackend.Adaptor(geom)]
+ s['select_params'] = [backend.Adapter(geom)]
return self._spatial_attribute(func, s, **kwargs)
def _geom_attribute(self, func, tolerance=0.05, **kwargs):
@@ -643,7 +716,7 @@
Geometry attribute (e.g., `centroid`, `point_on_surface`).
"""
s = {'select_field' : GeomField(),}
- if SpatialBackend.oracle:
+ if connections[self.db].ops.oracle:
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
s['procedure_args'] = {'tolerance' : tolerance}
return self._spatial_attribute(func, s, **kwargs)
@@ -660,7 +733,7 @@
'procedure_fmt' : '%(geo_col)s,%(geom)s',
'procedure_args' : {'geom' : geom},
}
- if SpatialBackend.oracle:
+ if connections[self.db].ops.oracle:
s['procedure_fmt'] += ',%(tolerance)s'
s['procedure_args']['tolerance'] = tolerance
return self._spatial_attribute(func, s, **kwargs)
@@ -677,16 +750,17 @@
# If so, it'll have to be added to the select related information
# (e.g., if 'location__point' was given as the field name).
self.query.add_select_related([field_name])
- self.query.pre_sql_setup()
+ compiler = self.query.get_compiler(self.db)
+ compiler.pre_sql_setup()
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)
+ return compiler._field_column(geo_field, rel_table)
elif not geo_field in opts.local_fields:
# This geographic field is inherited from another model, so we have to
# use the db table for the _parent_ model instead.
tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name)
- return self.query._field_column(geo_field, parent_model._meta.db_table)
+ return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table)
else:
- return self.query._field_column(geo_field)
+ return self.query.get_compiler(self.db)._field_column(geo_field)
class GeoValuesQuerySet(ValuesQuerySet):
def __init__(self, *args, **kwargs):