diff options
Diffstat (limited to 'django/contrib/gis/db/models/sql')
| -rw-r--r-- | django/contrib/gis/db/models/sql/aggregates.py | 36 | ||||
| -rw-r--r-- | django/contrib/gis/db/models/sql/query.py | 130 |
2 files changed, 121 insertions, 45 deletions
diff --git a/django/contrib/gis/db/models/sql/aggregates.py b/django/contrib/gis/db/models/sql/aggregates.py new file mode 100644 index 0000000000..ff76334249 --- /dev/null +++ b/django/contrib/gis/db/models/sql/aggregates.py @@ -0,0 +1,36 @@ +from django.db.models.sql.aggregates import * + +from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.backend import SpatialBackend + +if SpatialBackend.oracle: + geo_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))' +else: + geo_template = '%(function)s(%(field)s)' + +class GeoAggregate(Aggregate): + # Overriding the SQL template with the geographic one. + sql_template = geo_template + + is_extent = False + + def __init__(self, col, source=None, is_summary=False, **extra): + super(GeoAggregate, self).__init__(col, source, is_summary, **extra) + + # Can't use geographic aggregates on non-geometry fields. + if not isinstance(self.source, GeometryField): + raise ValueError('Geospatial aggregates only allowed on geometry fields.') + + # Making sure the SQL function is available for this spatial backend. + if not self.sql_function: + raise NotImplementedError('This aggregate functionality not implemented for your spatial backend.') + +class Extent(GeoAggregate): + is_extent = True + sql_function = SpatialBackend.extent + +class MakeLine(GeoAggregate): + sql_function = SpatialBackend.make_line + +class Union(GeoAggregate): + sql_function = SpatialBackend.unionagg diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 52f521d500..246ea0300f 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -5,6 +5,7 @@ from django.db.models.fields.related import ForeignKey from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.db.models.sql import aggregates as gis_aggregates_module from django.contrib.gis.db.models.sql.where import GeoWhereNode from django.contrib.gis.measure import Area, Distance @@ -12,12 +13,35 @@ from django.contrib.gis.measure import Area, Distance ALL_TERMS = sql.constants.QUERY_TERMS.copy() ALL_TERMS.update(SpatialBackend.gis_terms) +# Conversion functions used in normalizing geographic aggregates. +if SpatialBackend.postgis: + def convert_extent(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) + + def convert_geom(hex, geo_field): + if hex: return SpatialBackend.Geometry(hex) + else: return None +else: + def convert_extent(box): + raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') + + def convert_geom(clob, geo_field): + if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) + else: return None + class GeoQuery(sql.Query): """ A single spatial SQL query. """ # Overridding the valid query terms. query_terms = ALL_TERMS + aggregates_module = gis_aggregates_module #### Methods overridden from the base Query class #### def __init__(self, model, conn): @@ -25,7 +49,6 @@ class GeoQuery(sql.Query): # The following attributes are customized for the GeoQuerySet. # The GeoWhereNode and SpatialBackend classes contain backend-specific # routines and functions. - self.aggregate = False self.custom_select = {} self.transformed_srid = None self.extra_select_fields = {} @@ -34,7 +57,6 @@ class GeoQuery(sql.Query): obj = super(GeoQuery, self).clone(*args, **kwargs) # 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.transformed_srid = self.transformed_srid obj.extra_select_fields = self.extra_select_fields.copy() @@ -50,12 +72,12 @@ class GeoQuery(sql.Query): (without the table names) are given unique aliases. This is needed in some cases to avoid ambiguitity with nested queries. - This routine is overridden from Query to handle customized selection of + This routine is overridden from Query to handle customized selection of geometry columns. """ qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name - result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) + result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) for alias, col in self.extra_select.iteritems()] aliases = set(self.extra_select.keys()) if with_aliases: @@ -67,38 +89,53 @@ class GeoQuery(sql.Query): for col, field in izip(self.select, self.select_fields): if isinstance(col, (list, tuple)): r = self.get_field_select(field, col[0]) - if with_aliases and col[1] 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) + if with_aliases: + if col[1] 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('%s AS %s' % (r, col[1])) + aliases.add(r) + col_aliases.add(col[1]) else: result.append(r) aliases.add(r) col_aliases.add(col[1]) else: result.append(col.as_sql(quote_func=qn)) + if hasattr(col, 'alias'): aliases.add(col.alias) col_aliases.add(col.alias) + elif self.default_cols: cols, new_aliases = self.get_default_columns(with_aliases, col_aliases) result.extend(cols) aliases.update(new_aliases) + + result.extend([ + '%s%s' % ( + aggregate.as_sql(quote_func=qn), + alias is not None and ' AS %s' % alias or '' + ) + for alias, aggregate in self.aggregate_select.items() + ]) + # This loop customized for GeoQuery. - 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) + 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 @@ -112,7 +149,7 @@ class GeoQuery(sql.Query): Returns a list of strings, quoted appropriately for use in SQL directly, as well as a set of aliases used in the select statement. - This routine is overridden from Query to handle customized selection of + This routine is overridden from Query to handle customized selection of geometry columns. """ result = [] @@ -154,20 +191,10 @@ class GeoQuery(sql.Query): return result, None return result, aliases - def get_ordering(self): - """ - This routine is overridden to disable ordering for aggregate - spatial queries. - """ - if not self.aggregate: - return super(GeoQuery, self).get_ordering() - else: - return () - 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 + from extra selection SQL get resolved appropriately into Python objects. """ values = [] @@ -183,7 +210,7 @@ class GeoQuery(sql.Query): # Converting any extra selection values (e.g., geometries and # distance objects added by GeoQuerySet methods). - values = [self.convert_values(v, self.extra_select_fields.get(a, None)) + values = [self.convert_values(v, self.extra_select_fields.get(a, None)) for v, a in izip(row[rn_offset:index_start], aliases)] if SpatialBackend.oracle: # This is what happens normally in OracleQuery's `resolve_columns`. @@ -212,6 +239,19 @@ class GeoQuery(sql.Query): value = SpatialBackend.Geometry(value) return value + def resolve_aggregate(self, value, aggregate): + """ + Overridden from GeoQuery's normalize to handle the conversion of + GeoAggregate objects. + """ + if isinstance(aggregate, self.aggregates_module.GeoAggregate): + if aggregate.is_extent: + return convert_extent(value) + else: + return convert_geom(value, aggregate.source) + else: + return super(GeoQuery, self).resolve_aggregate(value, aggregate) + #### Routines unique to GeoQuery #### def get_extra_select_format(self, alias): sel_fmt = '%s' @@ -222,9 +262,9 @@ class GeoQuery(sql.Query): def get_field_select(self, fld, alias=None): """ Returns the SELECT SQL string for the given field. Figures out - if any custom selection SQL is needed for the column The `alias` - keyword may be used to manually specify the database table where - the column exists, if not in the model associated with this + if any custom selection SQL is needed for the column The `alias` + keyword may be used to manually specify the database table where + the column exists, if not in the model associated with this `GeoQuery`. """ sel_fmt = self.get_select_format(fld) @@ -263,15 +303,15 @@ class GeoQuery(sql.Query): """ Recursive utility routine for checking the given name parameter on the given model. Initially, the name parameter is a string, - of the field on the given model e.g., 'point', 'the_geom'. - Related model field strings like 'address__point', may also be + of the field on the given model e.g., 'point', 'the_geom'. + Related model field strings like 'address__point', may also be used. - If a GeometryField exists according to the given name parameter + 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 + # This takes into account the situation where the name is a # lookup to a related geographic field, e.g., 'address__point'. name_param = name_param.split(sql.constants.LOOKUP_SEP) name_param.reverse() # Reversing so list operates like a queue of related lookups. @@ -284,7 +324,7 @@ class GeoQuery(sql.Query): except (FieldDoesNotExist, IndexError): return False # TODO: ManyToManyField? - if isinstance(fld, GeometryField): + if isinstance(fld, GeometryField): return fld # A-OK. elif isinstance(fld, ForeignKey): # ForeignKey encountered, return the output of this utility called @@ -297,12 +337,12 @@ class GeoQuery(sql.Query): """ Helper function that returns the database column for the given field. The table and column are returned (quoted) in the proper format, e.g., - `"geoapp_city"."point"`. If `table_alias` is not specified, the + `"geoapp_city"."point"`. If `table_alias` is not specified, the database table associated with the model of this `GeoQuery` will be used. """ if table_alias is None: table_alias = self.model._meta.db_table - return "%s.%s" % (self.quote_name_unless_alias(table_alias), + return "%s.%s" % (self.quote_name_unless_alias(table_alias), self.connection.ops.quote_name(field.column)) def _geo_field(self, field_name=None): @@ -333,5 +373,5 @@ class DistanceField(object): # Rather than use GeometryField (which requires a SQL query # upon instantiation), use this lighter weight class. -class GeomField(object): +class GeomField(object): pass |
