diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index 63c01b146b..fd838821da 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -5,17 +5,14 @@ needed for GeoDjango. (1) GeoBackEndField, a base class needed for GeometryField. - (2) GeometryProxy, for lazy-instantiated geometries from the - database output. - (3) GIS_TERMS, a list of acceptable geographic lookup types for + (2) GIS_TERMS, a list of acceptable geographic lookup types for the backend. - (4) The `parse_lookup` function, used for spatial SQL construction by + (3) The `parse_lookup` function, used for spatial SQL construction by the GeoQuerySet. - (5) The `create_spatial_db`, and `get_geo_where_clause` + (4) The `create_spatial_db`, and `get_geo_where_clause` routines (needed by `parse_lookup`). - - Currently only PostGIS is supported, but someday backends will be added for - additional spatial databases (e.g., Oracle, DB2). + (5) The `SpatialBackend` object, which contains information specific + to the spatial backend. """ from types import StringType, UnicodeType from django.conf import settings @@ -26,7 +23,7 @@ from django.utils.datastructures import SortedDict from django.contrib.gis.geos import GEOSGeometry # These routines (needed by GeoManager), default to False. -ASGML, ASKML, DISTANCE, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False) +ASGML, ASKML, DISTANCE, EXTENT, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False, False) if settings.DATABASE_ENGINE == 'postgresql_psycopg2': # PostGIS is the spatial database, getting the rquired modules, @@ -34,7 +31,7 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2': from django.contrib.gis.db.backend.postgis import \ PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ create_spatial_db, get_geo_where_clause, \ - ASGML, ASKML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION, \ + ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \ MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2) SPATIAL_BACKEND = 'postgis' @@ -55,6 +52,18 @@ elif settings.DATABASE_ENGINE == 'mysql': else: raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) +class SpatialBackend(object): + "A container for properties of the Spatial Backend." + as_kml = ASKML + as_gml = ASGML + distance = DISTANCE + extent = EXTENT + name = SPATIAL_BACKEND + select = GEOM_SELECT + transform = TRANSFORM + union = UNION + version = VERSION + #### query.py overloaded functions #### # parse_lookup() and lookup_inner() are modified from their django/db/models/query.py # counterparts to support constructing SQL for geographic queries. diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 34a9b93768..334b1197c4 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -6,4 +6,4 @@ from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn from django.contrib.gis.db.backend.postgis.query import \ get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \ MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \ - ASKML, ASGML, DISTANCE, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT + ASKML, ASGML, DISTANCE, EXTENT, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 8387022004..f920a8f7bc 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1: ASKML = get_func('AsKML') ASGML = get_func('AsGML') DISTANCE = get_func('Distance') + EXTENT = get_func('extent') GEOM_FROM_TEXT = get_func('GeomFromText') GEOM_FROM_WKB = get_func('GeomFromWKB') TRANSFORM = get_func('Transform') diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 399988d1d6..8634dd9cee 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -10,6 +10,9 @@ class GeoManager(Manager): def distance(self, *args, **kwargs): return self.get_query_set().distance(*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) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index c289dedeb3..3e909c1c3b 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -6,13 +6,12 @@ from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict from django.contrib.gis.db.models.fields import GeometryField # parse_lookup depends on the spatial database backend. -from django.contrib.gis.db.backend import parse_lookup, \ - ASGML, ASKML, DISTANCE, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION, VERSION +from django.contrib.gis.db.backend import parse_lookup, SpatialBackend from django.contrib.gis.geos import GEOSGeometry # Shortcut booleans for determining the backend. -oracle = SPATIAL_BACKEND == 'oracle' -postgis = SPATIAL_BACKEND == 'postgis' +oracle = SpatialBackend.name == 'oracle' +postgis = SpatialBackend.name == 'postgis' class GeoQ(Q): "Geographical query encapsulation object." @@ -23,7 +22,7 @@ class GeoQ(Q): class GeoQuerySet(QuerySet): "Geographical-enabled QuerySet object." - + #### Overloaded QuerySet Routines #### def __init__(self, model=None): super(GeoQuerySet, self).__init__(model=model) @@ -37,10 +36,10 @@ class GeoQuerySet(QuerySet): # If GEOM_SELECT is defined in the backend, then it will be used # for the selection format of the geometry column. - if GEOM_SELECT: + if SpatialBackend.select: # Transformed geometries in Oracle use EWKT so that the SRID # on the transformed lazy geometries is set correctly). - self._geo_fmt = GEOM_SELECT + self._geo_fmt = SpatialBackend.select else: self._geo_fmt = '%s' @@ -259,6 +258,7 @@ class GeoQuerySet(QuerySet): given geometry in a `distance` attribute on each element of the GeoQuerySet. """ + DISTANCE = SpatialBackend.distance if not DISTANCE: raise ImproperlyConfigured('Distance() stored proecedure not available.') @@ -299,12 +299,55 @@ class GeoQuerySet(QuerySet): dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)} return self.extra(select=dist_select) + def extent(self, field_name=None): + """ + 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). + """ + EXTENT = SpatialBackend.extent + if not EXTENT: + raise ImproperlyConfigured('Extent stored procedure not available.') + + if not field_name: + field_name = self._get_geofield() + + field_col = self._geo_column(field_name) + if not field_col: + raise TypeError('Extent information only available on GeometryFields.') + + # Getting the SQL for the query. + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + return None + + # Constructing the query that will select the extent. + extent_sql = ('SELECT %s(%s)' % (EXTENT, field_col)) + sql + + # Getting a cursor, executing the query, and extracting the returned + # value from the extent function. + cursor = connection.cursor() + cursor.execute(extent_sql, 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): """ 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.') @@ -322,7 +365,7 @@ class GeoQuerySet(QuerySet): elif postgis: # PostGIS AsGML() aggregate function parameter order depends on the # version -- uggh. - major, minor1, minor2 = VERSION + 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, field_col, precision)} else: @@ -337,6 +380,7 @@ class GeoQuerySet(QuerySet): attribute on each element of the GeoQuerySet. """ # Is KML output supported? + ASKML = SpatialBackend.as_kml if not ASKML: raise ImproperlyConfigured('AsKML() stored procedure not available.') @@ -375,6 +419,7 @@ class GeoQuerySet(QuerySet): # Setting the key for the field's column with the custom SELECT SQL to # override the geometry column returned from the database. + TRANSFORM = SpatialBackend.transform if oracle: custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid) self._ewkt = srid @@ -391,6 +436,7 @@ class GeoQuerySet(QuerySet): Oracle backends only. """ # Making sure backend supports the Union stored procedure + UNION = SpatialBackend.union if not UNION: raise ImproperlyConfigured('Union stored procedure not available.') diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 6533319885..df2cfce80d 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -172,6 +172,20 @@ class GeoModelTest(unittest.TestCase): self.assertAlmostEqual(ptown.x, p.point.x, prec) self.assertAlmostEqual(ptown.y, p.point.y, prec) + @no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!) + def test05_extent(self): + "Testing the extent() GeoManager method." + # Reference query: + # `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');` + # => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203) + expected = (-96.8016128540039, 29.7633724212646, -95.3631439208984, 32.782058715820) + + qs = City.objects.filter(name__in=('Houston', 'Dallas')) + extent = qs.extent() + + for val, exp in zip(extent, expected): + self.assertAlmostEqual(exp, val, 8) + def test09_disjoint(self): "Testing the `disjoint` lookup type." if DISABLE: return diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 5475dd6c6e..dfcb80f0d1 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -112,7 +112,7 @@ from datetime import date, datetime from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist from django.contrib.gis.db.models.fields import GeometryField -from django.contrib.gis.db.backend import SPATIAL_BACKEND +from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.gdal import CoordTransform, DataSource, \ OGRException, OGRGeometry, OGRGeomType, SpatialReference from django.contrib.gis.gdal.field import \ @@ -506,7 +506,7 @@ class LayerMapping(object): # Getting the GeometryColumn object. try: db_table = self.model._meta.db_table - if SPATIAL_BACKEND == 'oracle': db_table = db_table.upper() + if SpatialBackend.name == 'oracle': db_table = db_table.upper() gc_kwargs = {GeometryColumns.table_name_col() : db_table} return GeometryColumns.objects.get(**gc_kwargs) except Exception, msg: