Fixed incomplete merge of geographic aggregates; added support for `Extent` aggregate to Oracle spatial backend. Refs #3566.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@9748 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
91e25f9ef8
commit
f671a5c961
|
@ -1,6 +1,9 @@
|
||||||
# Want to get everything from the 'normal' models package.
|
# Want to get everything from the 'normal' models package.
|
||||||
from django.db.models import *
|
from django.db.models import *
|
||||||
|
|
||||||
|
# Geographic aggregate functions
|
||||||
|
from django.contrib.gis.db.models.aggregates import *
|
||||||
|
|
||||||
# The GeoManager
|
# The GeoManager
|
||||||
from django.contrib.gis.db.models.manager import GeoManager
|
from django.contrib.gis.db.models.manager import GeoManager
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
from django.db.models import Aggregate
|
from django.db.models import Aggregate
|
||||||
|
from django.contrib.gis.db.backend import SpatialBackend
|
||||||
|
from django.contrib.gis.db.models.sql import GeomField
|
||||||
|
|
||||||
class Extent(Aggregate):
|
class GeoAggregate(Aggregate):
|
||||||
|
|
||||||
|
def add_to_query(self, query, alias, col, source, is_summary):
|
||||||
|
if hasattr(source, '_geom'):
|
||||||
|
# Doing additional setup on the Query object for spatial aggregates.
|
||||||
|
aggregate = getattr(query.aggregates_module, self.name)
|
||||||
|
|
||||||
|
# Adding a conversion class instance and any selection wrapping
|
||||||
|
# SQL (e.g., needed by Oracle).
|
||||||
|
if aggregate.conversion_class is GeomField:
|
||||||
|
query.extra_select_fields[alias] = GeomField()
|
||||||
|
if SpatialBackend.select:
|
||||||
|
query.custom_select[alias] = SpatialBackend.select
|
||||||
|
|
||||||
|
super(GeoAggregate, self).add_to_query(query, alias, col, source, is_summary)
|
||||||
|
|
||||||
|
class Extent(GeoAggregate):
|
||||||
name = 'Extent'
|
name = 'Extent'
|
||||||
|
|
||||||
class MakeLine(Aggregate):
|
class MakeLine(GeoAggregate):
|
||||||
name = 'MakeLine'
|
name = 'MakeLine'
|
||||||
|
|
||||||
class Union(Aggregate):
|
class Union(GeoAggregate):
|
||||||
name = 'Union'
|
name = 'Union'
|
||||||
|
|
|
@ -8,7 +8,6 @@ from django.contrib.gis.db.models.fields import GeometryField, PointField
|
||||||
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
|
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.measure import Area, Distance
|
||||||
from django.contrib.gis.models import get_srid_info
|
from django.contrib.gis.models import get_srid_info
|
||||||
qn = connection.ops.quote_name
|
|
||||||
|
|
||||||
# For backwards-compatibility; Q object should work just fine
|
# For backwards-compatibility; Q object should work just fine
|
||||||
# after queryset-refactor.
|
# after queryset-refactor.
|
||||||
|
@ -331,7 +330,7 @@ class GeoQuerySet(QuerySet):
|
||||||
if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance
|
if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance
|
||||||
|
|
||||||
# Calling the QuerySet.aggregate, and returning only the value of the aggregate.
|
# Calling the QuerySet.aggregate, and returning only the value of the aggregate.
|
||||||
return self.aggregate(_geoagg=aggregate(agg_col, **agg_kwargs))['_geoagg']
|
return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg']
|
||||||
|
|
||||||
def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
|
def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
|
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
|
||||||
|
from django.contrib.gis.db.models.sql.query import GeoQuery
|
||||||
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
||||||
|
|
|
@ -1,24 +1,70 @@
|
||||||
from django.db.models.sql.aggregates import *
|
from django.db.models.sql.aggregates import *
|
||||||
|
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
|
from django.contrib.gis.db.models.sql.conversion import GeomField
|
||||||
from django.contrib.gis.db.backend import SpatialBackend
|
from django.contrib.gis.db.backend import SpatialBackend
|
||||||
|
|
||||||
if SpatialBackend.oracle:
|
# Default SQL template for spatial aggregates.
|
||||||
|
geo_template = '%(function)s(%(field)s)'
|
||||||
|
|
||||||
|
# Default conversion functions for aggregates; will be overridden if implemented
|
||||||
|
# for the spatial backend.
|
||||||
|
def convert_extent(box):
|
||||||
|
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
|
||||||
|
|
||||||
|
def convert_geom(wkt, geo_field):
|
||||||
|
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
|
||||||
|
|
||||||
|
if SpatialBackend.postgis:
|
||||||
|
def convert_extent(box):
|
||||||
|
# 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
|
||||||
|
elif SpatialBackend.oracle:
|
||||||
|
# Oracle spatial aggregates need a tolerance.
|
||||||
geo_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))'
|
geo_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))'
|
||||||
else:
|
|
||||||
geo_template = '%(function)s(%(field)s)'
|
def convert_extent(clob):
|
||||||
|
if clob:
|
||||||
|
# Oracle returns a polygon for the extent, we construct
|
||||||
|
# the 4-tuple from the coordinates in the polygon.
|
||||||
|
poly = SpatialBackend.Geometry(clob.read())
|
||||||
|
shell = poly.shell
|
||||||
|
ll, ur = shell[0], shell[2]
|
||||||
|
xmin, ymin = ll
|
||||||
|
xmax, ymax = ur
|
||||||
|
return (xmin, ymin, xmax, ymax)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def convert_geom(clob, geo_field):
|
||||||
|
if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
|
||||||
|
else: return None
|
||||||
|
|
||||||
class GeoAggregate(Aggregate):
|
class GeoAggregate(Aggregate):
|
||||||
# Overriding the SQL template with the geographic one.
|
# Overriding the SQL template with the geographic one.
|
||||||
sql_template = geo_template
|
sql_template = geo_template
|
||||||
|
|
||||||
|
# Conversion class, if necessary.
|
||||||
|
conversion_class = None
|
||||||
|
|
||||||
|
# Flags for indicating the type of the aggregate.
|
||||||
is_extent = False
|
is_extent = False
|
||||||
|
|
||||||
def __init__(self, col, source=None, is_summary=False, **extra):
|
def __init__(self, col, source=None, is_summary=False, **extra):
|
||||||
super(GeoAggregate, self).__init__(col, source, is_summary, **extra)
|
super(GeoAggregate, self).__init__(col, source, is_summary, **extra)
|
||||||
|
|
||||||
|
if not self.is_extent and SpatialBackend.oracle:
|
||||||
|
self.extra.setdefault('tolerance', 0.05)
|
||||||
|
|
||||||
# Can't use geographic aggregates on non-geometry fields.
|
# Can't use geographic aggregates on non-geometry fields.
|
||||||
if not isinstance(self.source, GeometryField):
|
if not isinstance(self.source, GeometryField):
|
||||||
raise ValueError('Geospatial aggregates only allowed on geometry fields.')
|
raise ValueError('Geospatial aggregates only allowed on geometry fields.')
|
||||||
|
|
||||||
# Making sure the SQL function is available for this spatial backend.
|
# Making sure the SQL function is available for this spatial backend.
|
||||||
|
@ -28,9 +74,16 @@ class GeoAggregate(Aggregate):
|
||||||
class Extent(GeoAggregate):
|
class Extent(GeoAggregate):
|
||||||
is_extent = True
|
is_extent = True
|
||||||
sql_function = SpatialBackend.extent
|
sql_function = SpatialBackend.extent
|
||||||
|
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
# Have to change Extent's attributes here for Oracle.
|
||||||
|
Extent.conversion_class = GeomField
|
||||||
|
Extent.sql_template = '%(function)s(%(field)s)'
|
||||||
|
|
||||||
class MakeLine(GeoAggregate):
|
class MakeLine(GeoAggregate):
|
||||||
|
conversion_class = GeomField
|
||||||
sql_function = SpatialBackend.make_line
|
sql_function = SpatialBackend.make_line
|
||||||
|
|
||||||
class Union(GeoAggregate):
|
class Union(GeoAggregate):
|
||||||
|
conversion_class = GeomField
|
||||||
sql_function = SpatialBackend.unionagg
|
sql_function = SpatialBackend.unionagg
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
This module holds simple classes used by GeoQuery.convert_values
|
||||||
|
to convert geospatial values from the database.
|
||||||
|
"""
|
||||||
|
class BaseField(object):
|
||||||
|
def get_internal_type(self):
|
||||||
|
"Overloaded method so OracleQuery.convert_values doesn't balk."
|
||||||
|
return None
|
||||||
|
|
||||||
|
class AreaField(BaseField):
|
||||||
|
"Wrapper for Area values."
|
||||||
|
def __init__(self, area_att):
|
||||||
|
self.area_att = area_att
|
||||||
|
|
||||||
|
class DistanceField(BaseField):
|
||||||
|
"Wrapper for Distance values."
|
||||||
|
def __init__(self, distance_att):
|
||||||
|
self.distance_att = distance_att
|
||||||
|
|
||||||
|
class GeomField(BaseField):
|
||||||
|
"""
|
||||||
|
Wrapper for Geometry values. It is a lightweight alternative to
|
||||||
|
using GeometryField (which requires a SQL query upon instantiation).
|
||||||
|
"""
|
||||||
|
pass
|
|
@ -6,6 +6,7 @@ from django.db.models.fields.related import ForeignKey
|
||||||
from django.contrib.gis.db.backend import SpatialBackend
|
from django.contrib.gis.db.backend import SpatialBackend
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
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 import aggregates as gis_aggregates_module
|
||||||
|
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
|
||||||
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
||||||
from django.contrib.gis.measure import Area, Distance
|
from django.contrib.gis.measure import Area, Distance
|
||||||
|
|
||||||
|
@ -13,28 +14,6 @@ from django.contrib.gis.measure import Area, Distance
|
||||||
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
|
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
|
||||||
ALL_TERMS.update(SpatialBackend.gis_terms)
|
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):
|
class GeoQuery(sql.Query):
|
||||||
"""
|
"""
|
||||||
A single spatial SQL query.
|
A single spatial SQL query.
|
||||||
|
@ -118,7 +97,7 @@ class GeoQuery(sql.Query):
|
||||||
|
|
||||||
result.extend([
|
result.extend([
|
||||||
'%s%s' % (
|
'%s%s' % (
|
||||||
aggregate.as_sql(quote_func=qn),
|
self.get_extra_select_format(alias) % aggregate.as_sql(quote_func=qn),
|
||||||
alias is not None and ' AS %s' % alias or ''
|
alias is not None and ' AS %s' % alias or ''
|
||||||
)
|
)
|
||||||
for alias, aggregate in self.aggregate_select.items()
|
for alias, aggregate in self.aggregate_select.items()
|
||||||
|
@ -228,7 +207,7 @@ class GeoQuery(sql.Query):
|
||||||
"""
|
"""
|
||||||
if SpatialBackend.oracle:
|
if SpatialBackend.oracle:
|
||||||
# Running through Oracle's first.
|
# Running through Oracle's first.
|
||||||
value = super(GeoQuery, self).convert_values(value, field)
|
value = super(GeoQuery, self).convert_values(value, field or GeomField())
|
||||||
if isinstance(field, DistanceField):
|
if isinstance(field, DistanceField):
|
||||||
# Using the field's distance attribute, can instantiate
|
# Using the field's distance attribute, can instantiate
|
||||||
# `Distance` with the right context.
|
# `Distance` with the right context.
|
||||||
|
@ -246,9 +225,9 @@ class GeoQuery(sql.Query):
|
||||||
"""
|
"""
|
||||||
if isinstance(aggregate, self.aggregates_module.GeoAggregate):
|
if isinstance(aggregate, self.aggregates_module.GeoAggregate):
|
||||||
if aggregate.is_extent:
|
if aggregate.is_extent:
|
||||||
return convert_extent(value)
|
return self.aggregates_module.convert_extent(value)
|
||||||
else:
|
else:
|
||||||
return convert_geom(value, aggregate.source)
|
return self.aggregates_module.convert_geom(value, aggregate.source)
|
||||||
else:
|
else:
|
||||||
return super(GeoQuery, self).resolve_aggregate(value, aggregate)
|
return super(GeoQuery, self).resolve_aggregate(value, aggregate)
|
||||||
|
|
||||||
|
@ -361,17 +340,3 @@ class GeoQuery(sql.Query):
|
||||||
# Otherwise, check by the given field name -- which may be
|
# Otherwise, check by the given field name -- which may be
|
||||||
# a lookup to a _related_ geographic field.
|
# a lookup to a _related_ geographic field.
|
||||||
return self._check_geo_field(self.model, field_name)
|
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
|
|
||||||
|
|
Loading…
Reference in New Issue