diff -r b758351d191f -r cc9b7e14412b web/lib/django/contrib/gis/db/models/query.py --- 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):