diff options
Diffstat (limited to 'webapp/django/contrib/gis/db')
31 files changed, 2777 insertions, 0 deletions
diff --git a/webapp/django/contrib/gis/db/__init__.py b/webapp/django/contrib/gis/db/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/webapp/django/contrib/gis/db/__init__.py diff --git a/webapp/django/contrib/gis/db/backend/__init__.py b/webapp/django/contrib/gis/db/backend/__init__.py new file mode 100644 index 0000000000..172c1268a7 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/__init__.py @@ -0,0 +1,18 @@ +""" + This module provides the backend for spatial SQL construction with Django. + + Specifically, this module will import the correct routines and modules + needed for GeoDjango to interface with the spatial database. +""" +from django.conf import settings +from django.contrib.gis.db.backend.util import gqn + +# Retrieving the necessary settings from the backend. +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend +elif settings.DATABASE_ENGINE == 'oracle': + from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend +elif settings.DATABASE_ENGINE == 'mysql': + from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend +else: + raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) diff --git a/webapp/django/contrib/gis/db/backend/adaptor.py b/webapp/django/contrib/gis/db/backend/adaptor.py new file mode 100644 index 0000000000..b2397e61dd --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/adaptor.py @@ -0,0 +1,14 @@ +class WKTAdaptor(object): + """ + This provides an adaptor for Geometries sent to the + MySQL and Oracle database backends. + """ + def __init__(self, geom): + self.wkt = geom.wkt + self.srid = geom.srid + + def __eq__(self, other): + return self.wkt == other.wkt and self.srid == other.srid + + def __str__(self): + return self.wkt diff --git a/webapp/django/contrib/gis/db/backend/base.py b/webapp/django/contrib/gis/db/backend/base.py new file mode 100644 index 0000000000..d45ac7b6f1 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/base.py @@ -0,0 +1,29 @@ +""" + This module holds the base `SpatialBackend` object, which is + instantiated by each spatial backend with the features it has. +""" +# TODO: Create a `Geometry` protocol and allow user to use +# different Geometry objects -- for now we just use GEOSGeometry. +from django.contrib.gis.geos import GEOSGeometry, GEOSException + +class BaseSpatialBackend(object): + Geometry = GEOSGeometry + GeometryException = GEOSException + + def __init__(self, **kwargs): + kwargs.setdefault('distance_functions', {}) + kwargs.setdefault('limited_where', {}) + for k, v in kwargs.iteritems(): setattr(self, k, v) + + def __getattr__(self, name): + """ + All attributes of the spatial backend return False by default. + """ + try: + return self.__dict__[name] + except KeyError: + return False + + + + diff --git a/webapp/django/contrib/gis/db/backend/mysql/__init__.py b/webapp/django/contrib/gis/db/backend/mysql/__init__.py new file mode 100644 index 0000000000..0484e5f9b2 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/mysql/__init__.py @@ -0,0 +1,13 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.adaptor import WKTAdaptor +from django.contrib.gis.db.backend.mysql.creation import create_spatial_db +from django.contrib.gis.db.backend.mysql.field import MySQLGeoField +from django.contrib.gis.db.backend.mysql.query import * + +SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True, + gis_terms=MYSQL_GIS_TERMS, + select=GEOM_SELECT, + Adaptor=WKTAdaptor, + Field=MySQLGeoField) diff --git a/webapp/django/contrib/gis/db/backend/mysql/creation.py b/webapp/django/contrib/gis/db/backend/mysql/creation.py new file mode 100644 index 0000000000..3da21a0cdd --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/mysql/creation.py @@ -0,0 +1,5 @@ + +def create_spatial_db(test=True, verbosity=1, autoclobber=False): + if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py') + from django.db import connection + connection.creation.create_test_db(verbosity, autoclobber) diff --git a/webapp/django/contrib/gis/db/backend/mysql/field.py b/webapp/django/contrib/gis/db/backend/mysql/field.py new file mode 100644 index 0000000000..f3151f93ff --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/mysql/field.py @@ -0,0 +1,53 @@ +from django.db import connection +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT + +# Quotename & geographic quotename, respectively. +qn = connection.ops.quote_name + +class MySQLGeoField(Field): + """ + The backend-specific geographic field for MySQL. + """ + + def _geom_index(self, style, db_table): + """ + Creates a spatial index for the geometry column. If MyISAM tables are + used an R-Tree index is created, otherwise a B-Tree index is created. + Thus, for best spatial performance, you should use MyISAM tables + (which do not support transactions). For more information, see Ch. + 16.6.1 of the MySQL 5.0 documentation. + """ + + # Getting the index name. + idx_name = '%s_%s_id' % (db_table, self.column) + + sql = style.SQL_KEYWORD('CREATE SPATIAL INDEX ') + \ + style.SQL_TABLE(qn(idx_name)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + '(' + \ + style.SQL_FIELD(qn(self.column)) + ');' + return sql + + def _post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. + """ + # Getting the geometric index for this Geometry column. + if self._index: + return (self._geom_index(style, db_table),) + else: + return () + + def db_type(self): + "The OpenGIS name is returned for the MySQL database column type." + return self._geom + + def get_placeholder(self, value): + """ + The placeholder here has to include MySQL's WKT constructor. Because + MySQL does not support spatial transformations, there is no need to + modify the placeholder based on the contents of the given value. + """ + return '%s(%%s)' % GEOM_FROM_TEXT diff --git a/webapp/django/contrib/gis/db/backend/mysql/query.py b/webapp/django/contrib/gis/db/backend/mysql/query.py new file mode 100644 index 0000000000..2fa984f325 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/mysql/query.py @@ -0,0 +1,59 @@ +""" + This module contains the spatial lookup types, and the `get_geo_where_clause` + routine for MySQL. + + Please note that MySQL only supports bounding box queries, also + known as MBRs (Minimum Bounding Rectangles). Moreover, spatial + indices may only be used on MyISAM tables -- if you need + transactions, take a look at PostGIS. +""" +from django.db import connection +qn = connection.ops.quote_name + +# To ease implementation, WKT is passed to/from MySQL. +GEOM_FROM_TEXT = 'GeomFromText' +GEOM_FROM_WKB = 'GeomFromWKB' +GEOM_SELECT = 'AsText(%s)' + +# WARNING: MySQL is NOT compliant w/the OpenGIS specification and +# _every_ one of these lookup types is on the _bounding box_ only. +MYSQL_GIS_FUNCTIONS = { + 'bbcontains' : 'MBRContains', # For consistency w/PostGIS API + 'bboverlaps' : 'MBROverlaps', # .. .. + 'contained' : 'MBRWithin', # .. .. + 'contains' : 'MBRContains', + 'disjoint' : 'MBRDisjoint', + 'equals' : 'MBREqual', + 'exact' : 'MBREqual', + 'intersects' : 'MBRIntersects', + 'overlaps' : 'MBROverlaps', + 'same_as' : 'MBREqual', + 'touches' : 'MBRTouches', + 'within' : 'MBRWithin', + } + +# This lookup type does not require a mapping. +MISC_TERMS = ['isnull'] + +# Assacceptable lookup types for Oracle spatial. +MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys() +MYSQL_GIS_TERMS += MISC_TERMS +MYSQL_GIS_TERMS = dict((term, None) for term in MYSQL_GIS_TERMS) # Making dictionary + +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in MySQL spatial SQL construction." + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + + # See if a MySQL Geometry function matches the lookup type next + lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False) + if lookup_info: + return "%s(%s, %%s)" % (lookup_info, geo_col) + + # Handling 'isnull' lookup type + # TODO: Is this needed because MySQL cannot handle NULL + # geometries in its spatial indices. + if lookup_type == 'isnull': + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/webapp/django/contrib/gis/db/backend/oracle/__init__.py b/webapp/django/contrib/gis/db/backend/oracle/__init__.py new file mode 100644 index 0000000000..3eee56ea23 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/__init__.py @@ -0,0 +1,31 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor +from django.contrib.gis.db.backend.oracle.creation import create_spatial_db +from django.contrib.gis.db.backend.oracle.field import OracleSpatialField +from django.contrib.gis.db.backend.oracle.query import * + +SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True, + area=AREA, + centroid=CENTROID, + difference=DIFFERENCE, + distance=DISTANCE, + distance_functions=DISTANCE_FUNCTIONS, + gis_terms=ORACLE_SPATIAL_TERMS, + gml=ASGML, + intersection=INTERSECTION, + length=LENGTH, + limited_where = {'relate' : None}, + num_geom=NUM_GEOM, + num_points=NUM_POINTS, + perimeter=LENGTH, + point_on_surface=POINT_ON_SURFACE, + select=GEOM_SELECT, + sym_difference=SYM_DIFFERENCE, + transform=TRANSFORM, + unionagg=UNIONAGG, + union=UNION, + Adaptor=OracleSpatialAdaptor, + Field=OracleSpatialField, + ) diff --git a/webapp/django/contrib/gis/db/backend/oracle/adaptor.py b/webapp/django/contrib/gis/db/backend/oracle/adaptor.py new file mode 100644 index 0000000000..95dc265795 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/adaptor.py @@ -0,0 +1,5 @@ +from cx_Oracle import CLOB +from django.contrib.gis.db.backend.adaptor import WKTAdaptor + +class OracleSpatialAdaptor(WKTAdaptor): + input_size = CLOB diff --git a/webapp/django/contrib/gis/db/backend/oracle/creation.py b/webapp/django/contrib/gis/db/backend/oracle/creation.py new file mode 100644 index 0000000000..d9b53d2049 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/creation.py @@ -0,0 +1,6 @@ + +def create_spatial_db(test=True, verbosity=1, autoclobber=False): + "A wrapper over the Oracle `create_test_db` routine." + if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py') + from django.db import connection + connection.creation.create_test_db(verbosity, autoclobber) diff --git a/webapp/django/contrib/gis/db/backend/oracle/field.py b/webapp/django/contrib/gis/db/backend/oracle/field.py new file mode 100644 index 0000000000..22625f5e10 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/field.py @@ -0,0 +1,103 @@ +from django.db import connection +from django.db.backends.util import truncate_name +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.util import gqn +from django.contrib.gis.db.backend.oracle.query import TRANSFORM + +# Quotename & geographic quotename, respectively. +qn = connection.ops.quote_name + +class OracleSpatialField(Field): + """ + The backend-specific geographic field for Oracle Spatial. + """ + + empty_strings_allowed = False + + def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs): + """ + Oracle Spatial backend needs to have the extent -- for projected coordinate + systems _you must define the extent manually_, since the coordinates are + for geodetic systems. The `tolerance` keyword specifies the tolerance + for error (in meters), and defaults to 0.05 (5 centimeters). + """ + # Oracle Spatial specific keyword arguments. + self._extent = extent + self._tolerance = tolerance + # Calling the Django field initialization. + super(OracleSpatialField, self).__init__(**kwargs) + + def _add_geom(self, style, db_table): + """ + Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA + table. + """ + + # Checking the dimensions. + # TODO: Add support for 3D geometries. + if self._dim != 2: + raise Exception('3D geometries not yet supported on Oracle Spatial backend.') + + # Constructing the SQL that will be used to insert information about + # the geometry column into the USER_GSDO_GEOM_METADATA table. + meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \ + style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \ + ' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \ + style.SQL_KEYWORD(' VALUES ') + '(\n ' + \ + style.SQL_TABLE(gqn(db_table)) + ',\n ' + \ + style.SQL_FIELD(gqn(self.column)) + ',\n ' + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ + ("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \ + style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \ + ("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \ + ' %s\n );' % self._srid + return meta_sql + + def _geom_index(self, style, db_table): + "Creates an Oracle Geometry index (R-tree) for this geometry field." + + # Getting the index name, Oracle doesn't allow object + # names > 30 characters. + idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30) + + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(qn(idx_name)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + '(' + \ + style.SQL_FIELD(qn(self.column)) + ') ' + \ + style.SQL_KEYWORD('INDEXTYPE IS ') + \ + style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';' + return sql + + def post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. + """ + # Getting the meta geometry information. + post_sql = self._add_geom(style, db_table) + + # Getting the geometric index for this Geometry column. + if self._index: + return (post_sql, self._geom_index(style, db_table)) + else: + return (post_sql,) + + def db_type(self): + "The Oracle geometric data type is MDSYS.SDO_GEOMETRY." + return 'MDSYS.SDO_GEOMETRY' + + def get_placeholder(self, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + SDO_CS.TRANSFORM() function call. + """ + if value is None: + return '%s' + elif value.srid != self._srid: + # Adding Transform() to the SQL placeholder. + return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid) + else: + return 'SDO_GEOMETRY(%%s, %s)' % self._srid diff --git a/webapp/django/contrib/gis/db/backend/oracle/models.py b/webapp/django/contrib/gis/db/backend/oracle/models.py new file mode 100644 index 0000000000..c740b48efe --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/models.py @@ -0,0 +1,49 @@ +""" + The GeometryColumns and SpatialRefSys models for the Oracle spatial + backend. + + It should be noted that Oracle Spatial does not have database tables + named according to the OGC standard, so the closest analogs are used. + For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns + model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model. +""" +from django.db import models +from django.contrib.gis.models import SpatialRefSysMixin + +class GeometryColumns(models.Model): + "Maps to the Oracle USER_SDO_GEOM_METADATA table." + table_name = models.CharField(max_length=32) + column_name = models.CharField(max_length=1024) + srid = models.IntegerField(primary_key=True) + # TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY). + class Meta: + db_table = 'USER_SDO_GEOM_METADATA' + + @classmethod + def table_name_col(cls): + return 'table_name' + + def __unicode__(self): + return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid) + +class SpatialRefSys(models.Model, SpatialRefSysMixin): + "Maps to the Oracle MDSYS.CS_SRS table." + cs_name = models.CharField(max_length=68) + srid = models.IntegerField(primary_key=True) + auth_srid = models.IntegerField() + auth_name = models.CharField(max_length=256) + wktext = models.CharField(max_length=2046) + #cs_bounds = models.GeometryField() + + class Meta: + # TODO: Figure out way to have this be MDSYS.CS_SRS without + # having django's quoting mess up the SQL. + db_table = 'CS_SRS' + + @property + def wkt(self): + return self.wktext + + @classmethod + def wkt_col(cls): + return 'wktext' diff --git a/webapp/django/contrib/gis/db/backend/oracle/query.py b/webapp/django/contrib/gis/db/backend/oracle/query.py new file mode 100644 index 0000000000..dcf6f67ae2 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/oracle/query.py @@ -0,0 +1,154 @@ +""" + This module contains the spatial lookup types, and the `get_geo_where_clause` + routine for Oracle Spatial. + + Please note that WKT support is broken on the XE version, and thus + this backend will not work on such platforms. Specifically, XE lacks + support for an internal JVM, and Java libraries are required to use + the WKT constructors. +""" +import re +from decimal import Decimal +from django.db import connection +from django.contrib.gis.db.backend.util import SpatialFunction +from django.contrib.gis.measure import Distance +qn = connection.ops.quote_name + +# The GML, distance, transform, and union procedures. +AREA = 'SDO_GEOM.SDO_AREA' +ASGML = 'SDO_UTIL.TO_GMLGEOMETRY' +CENTROID = 'SDO_GEOM.SDO_CENTROID' +DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE' +DISTANCE = 'SDO_GEOM.SDO_DISTANCE' +EXTENT = 'SDO_AGGR_MBR' +INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION' +LENGTH = 'SDO_GEOM.SDO_LENGTH' +NUM_GEOM = 'SDO_UTIL.GETNUMELEM' +NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES' +POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE' +SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR' +TRANSFORM = 'SDO_CS.TRANSFORM' +UNION = 'SDO_GEOM.SDO_UNION' +UNIONAGG = 'SDO_AGGR_UNION' + +# We want to get SDO Geometries as WKT because it is much easier to +# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings. +# However, this adversely affects performance (i.e., Java is called +# to convert to WKT on every query). If someone wishes to write a +# SDO_GEOMETRY(...) parser in Python, let me know =) +GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' + +#### Classes used in constructing Oracle spatial SQL #### +class SDOOperation(SpatialFunction): + "Base class for SDO* Oracle operations." + def __init__(self, func, **kwargs): + kwargs.setdefault('operator', '=') + kwargs.setdefault('result', 'TRUE') + kwargs.setdefault('end_subst', ") %s '%s'") + super(SDOOperation, self).__init__(func, **kwargs) + +class SDODistance(SpatialFunction): + "Class for Distance queries." + def __init__(self, op, tolerance=0.05): + super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance, + operator=op, result='%%s') + +class SDOGeomRelate(SpatialFunction): + "Class for using SDO_GEOM.RELATE." + def __init__(self, mask, tolerance=0.05): + # SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance. + # Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE'). + end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask) + beg_subst = "%%s(%%s, '%s'" % mask + super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst) + +class SDORelate(SpatialFunction): + "Class for using SDO_RELATE." + masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON' + mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I) + def __init__(self, mask): + func = 'SDO_RELATE' + if not self.mask_regex.match(mask): + raise ValueError('Invalid %s mask: "%s"' % (func, mask)) + super(SDORelate, self).__init__(func, end_subst=", 'mask=%s') = 'TRUE'" % mask) + +#### Lookup type mapping dictionaries of Oracle spatial operations #### + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +DISTANCE_FUNCTIONS = { + 'distance_gt' : (SDODistance('>'), dtypes), + 'distance_gte' : (SDODistance('>='), dtypes), + 'distance_lt' : (SDODistance('<'), dtypes), + 'distance_lte' : (SDODistance('<='), dtypes), + 'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', + beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes), + } + +ORACLE_GEOMETRY_FUNCTIONS = { + 'contains' : SDOOperation('SDO_CONTAINS'), + 'coveredby' : SDOOperation('SDO_COVEREDBY'), + 'covers' : SDOOperation('SDO_COVERS'), + 'disjoint' : SDOGeomRelate('DISJOINT'), + 'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? + 'equals' : SDOOperation('SDO_EQUAL'), + 'exact' : SDOOperation('SDO_EQUAL'), + 'overlaps' : SDOOperation('SDO_OVERLAPS'), + 'same_as' : SDOOperation('SDO_EQUAL'), + 'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch' + 'touches' : SDOOperation('SDO_TOUCH'), + 'within' : SDOOperation('SDO_INSIDE'), + } +ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) + +# This lookup type does not require a mapping. +MISC_TERMS = ['isnull'] + +# Acceptable lookup types for Oracle spatial. +ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys() +ORACLE_SPATIAL_TERMS += MISC_TERMS +ORACLE_SPATIAL_TERMS = dict((term, None) for term in ORACLE_SPATIAL_TERMS) # Making dictionary for fast lookups + +#### The `get_geo_where_clause` function for Oracle #### +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in Oracle spatial SQL construction." + # Getting the quoted table name as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + + # See if a Oracle Geometry function matches the lookup type next + lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False) + if lookup_info: + # Lookup types that are tuples take tuple arguments, e.g., 'relate' and + # 'dwithin' lookup types. + if isinstance(lookup_info, tuple): + # First element of tuple is lookup type, second element is the type + # of the expected argument (e.g., str, float) + sdo_op, arg_type = lookup_info + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(geo_annot.value, tuple): + raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) + if len(geo_annot.value) != 2: + raise ValueError('2-element tuple required for %s lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(geo_annot.value[1], arg_type): + raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) + + if lookup_type == 'relate': + # The SDORelate class handles construction for these queries, + # and verifies the mask argument. + return sdo_op(geo_annot.value[1]).as_sql(geo_col) + else: + # Otherwise, just call the `as_sql` method on the SDOOperation instance. + return sdo_op.as_sql(geo_col) + else: + # Lookup info is a SDOOperation instance, whose `as_sql` method returns + # the SQL necessary for the geometry function call. For example: + # SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE' + return lookup_info.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/webapp/django/contrib/gis/db/backend/postgis/__init__.py b/webapp/django/contrib/gis/db/backend/postgis/__init__.py new file mode 100644 index 0000000000..8a4d09e0d5 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/__init__.py @@ -0,0 +1,42 @@ +__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend'] + +from django.contrib.gis.db.backend.base import BaseSpatialBackend +from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor +from django.contrib.gis.db.backend.postgis.creation import create_spatial_db +from django.contrib.gis.db.backend.postgis.field import PostGISField +from django.contrib.gis.db.backend.postgis.query import * + +SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, + area=AREA, + centroid=CENTROID, + difference=DIFFERENCE, + distance=DISTANCE, + distance_functions=DISTANCE_FUNCTIONS, + distance_sphere=DISTANCE_SPHERE, + distance_spheroid=DISTANCE_SPHEROID, + envelope=ENVELOPE, + extent=EXTENT, + gis_terms=POSTGIS_TERMS, + gml=ASGML, + intersection=INTERSECTION, + kml=ASKML, + length=LENGTH, + length_spheroid=LENGTH_SPHEROID, + make_line=MAKE_LINE, + mem_size=MEM_SIZE, + num_geom=NUM_GEOM, + num_points=NUM_POINTS, + perimeter=PERIMETER, + point_on_surface=POINT_ON_SURFACE, + scale=SCALE, + select=GEOM_SELECT, + svg=ASSVG, + sym_difference=SYM_DIFFERENCE, + transform=TRANSFORM, + translate=TRANSLATE, + union=UNION, + unionagg=UNIONAGG, + version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2), + Adaptor=PostGISAdaptor, + Field=PostGISField, + ) diff --git a/webapp/django/contrib/gis/db/backend/postgis/adaptor.py b/webapp/django/contrib/gis/db/backend/postgis/adaptor.py new file mode 100644 index 0000000000..c094a9825a --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/adaptor.py @@ -0,0 +1,33 @@ +""" + This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. +""" + +from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB +from psycopg2 import Binary +from psycopg2.extensions import ISQLQuote + +class PostGISAdaptor(object): + def __init__(self, geom): + "Initializes on the geometry." + # Getting the WKB (in string form, to allow easy pickling of + # the adaptor) and the SRID from the geometry. + self.wkb = str(geom.wkb) + self.srid = geom.srid + + def __conform__(self, proto): + # Does the given protocol conform to what Psycopg2 expects? + if proto == ISQLQuote: + return self + else: + raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?') + + def __eq__(self, other): + return (self.wkb == other.wkb) and (self.srid == other.srid) + + def __str__(self): + return self.getquoted() + + def getquoted(self): + "Returns a properly quoted string for use in PostgreSQL/PostGIS." + # Want to use WKB, so wrap with psycopg2 Binary() to quote properly. + return "%s(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1) diff --git a/webapp/django/contrib/gis/db/backend/postgis/creation.py b/webapp/django/contrib/gis/db/backend/postgis/creation.py new file mode 100644 index 0000000000..44d9346364 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/creation.py @@ -0,0 +1,224 @@ +import os, re, sys + +from django.conf import settings +from django.core.management import call_command +from django.db import connection +from django.db.backends.creation import TEST_DATABASE_PREFIX + +def getstatusoutput(cmd): + "A simpler version of getstatusoutput that works on win32 platforms." + stdin, stdout, stderr = os.popen3(cmd) + output = stdout.read() + if output.endswith('\n'): output = output[:-1] + status = stdin.close() + return status, output + +def create_lang(db_name, verbosity=1): + "Sets up the pl/pgsql language on the given database." + + # Getting the command-line options for the shell command + options = get_cmd_options(db_name) + + # Constructing the 'createlang' command. + createlang_cmd = 'createlang %splpgsql' % options + if verbosity >= 1: print createlang_cmd + + # Must have database super-user privileges to execute createlang -- it must + # also be in your path. + status, output = getstatusoutput(createlang_cmd) + + # Checking the status of the command, 0 => execution successful + if status: + raise Exception("Error executing 'plpgsql' command: %s\n" % output) + +def _create_with_cursor(db_name, verbosity=1, autoclobber=False): + "Creates database with psycopg2 cursor." + + # Constructing the necessary SQL to create the database (the DATABASE_USER + # must possess the privileges to create a database) + create_sql = 'CREATE DATABASE %s' % connection.ops.quote_name(db_name) + if settings.DATABASE_USER: + create_sql += ' OWNER %s' % settings.DATABASE_USER + + cursor = connection.cursor() + connection.creation.set_autocommit() + + try: + # Trying to create the database first. + cursor.execute(create_sql) + #print create_sql + except Exception, e: + # Drop and recreate, if necessary. + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_db(db_name) + if verbosity >= 1: print 'Creating new spatial database...' + cursor.execute(create_sql) + else: + raise Exception('Spatial Database Creation canceled.') + +created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') +def _create_with_shell(db_name, verbosity=1, autoclobber=False): + """ + If no spatial database already exists, then using a cursor will not work. + Thus, a `createdb` command will be issued through the shell to bootstrap + creation of the spatial database. + """ + + # Getting the command-line options for the shell command + options = get_cmd_options(False) + create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name) + if verbosity >= 1: print create_cmd + + # Attempting to create the database. + status, output = getstatusoutput(create_cmd) + + if status: + if created_regex.match(output): + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_cmd = 'dropdb %s%s' % (options, db_name) + status, output = getstatusoutput(drop_cmd) + if status != 0: + raise Exception('Could not drop database %s: %s' % (db_name, output)) + if verbosity >= 1: print 'Creating new spatial database...' + status, output = getstatusoutput(create_cmd) + if status != 0: + raise Exception('Could not create database after dropping: %s' % output) + else: + raise Exception('Spatial Database Creation canceled.') + else: + raise Exception('Unknown error occurred in creating database: %s' % output) + +def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): + "Creates a spatial database based on the settings." + + # Making sure we're using PostgreSQL and psycopg2 + if settings.DATABASE_ENGINE != 'postgresql_psycopg2': + raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.') + + # Getting the spatial database name + if test: + db_name = get_spatial_db(test=True) + _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) + else: + db_name = get_spatial_db() + _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber) + + # Creating the db language, does not need to be done on NT platforms + # since the PostGIS installer enables this capability. + if os.name != 'nt': + create_lang(db_name, verbosity=verbosity) + + # Now adding in the PostGIS routines. + load_postgis_sql(db_name, verbosity=verbosity) + + if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name + + # Closing the connection + connection.close() + settings.DATABASE_NAME = db_name + + # Syncing the database + call_command('syncdb', verbosity=verbosity, interactive=interactive) + +def drop_db(db_name=False, test=False): + """ + Drops the given database (defaults to what is returned from + get_spatial_db()). All exceptions are propagated up to the caller. + """ + if not db_name: db_name = get_spatial_db(test=test) + cursor = connection.cursor() + cursor.execute('DROP DATABASE %s' % connection.ops.quote_name(db_name)) + +def get_cmd_options(db_name): + "Obtains the command-line PostgreSQL connection options for shell commands." + # The db_name parameter is optional + options = '' + if db_name: + options += '-d %s ' % db_name + if settings.DATABASE_USER: + options += '-U %s ' % settings.DATABASE_USER + if settings.DATABASE_HOST: + options += '-h %s ' % settings.DATABASE_HOST + if settings.DATABASE_PORT: + options += '-p %s ' % settings.DATABASE_PORT + return options + +def get_spatial_db(test=False): + """ + Returns the name of the spatial database. The 'test' keyword may be set + to return the test spatial database name. + """ + if test: + if settings.TEST_DATABASE_NAME: + test_db_name = settings.TEST_DATABASE_NAME + else: + test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME + return test_db_name + else: + if not settings.DATABASE_NAME: + raise Exception('must configure DATABASE_NAME in settings.py') + return settings.DATABASE_NAME + +def load_postgis_sql(db_name, verbosity=1): + """ + This routine loads up the PostGIS SQL files lwpostgis.sql and + spatial_ref_sys.sql. + """ + + # Getting the path to the PostGIS SQL + try: + # POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the + # PostGIS SQL files are located. This is especially useful on Win32 + # platforms since the output of pg_config looks like "C:/PROGRA~1/..". + sql_path = settings.POSTGIS_SQL_PATH + except AttributeError: + status, sql_path = getstatusoutput('pg_config --sharedir') + if status: + sql_path = '/usr/local/share' + + # The PostGIS SQL post-creation files. + lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql') + srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql') + if not os.path.isfile(lwpostgis_file): + raise Exception('Could not find PostGIS function definitions in %s' % lwpostgis_file) + if not os.path.isfile(srefsys_file): + raise Exception('Could not find PostGIS spatial reference system definitions in %s' % srefsys_file) + + # Getting the psql command-line options, and command format. + options = get_cmd_options(db_name) + cmd_fmt = 'psql %s-f "%%s"' % options + + # Now trying to load up the PostGIS functions + cmd = cmd_fmt % lwpostgis_file + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status: + raise Exception('Error in loading PostGIS lwgeometry routines.') + + # Now trying to load up the Spatial Reference System table + cmd = cmd_fmt % srefsys_file + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status: + raise Exception('Error in loading PostGIS spatial_ref_sys table.') + + # Setting the permissions because on Windows platforms the owner + # of the spatial_ref_sys and geometry_columns tables is always + # the postgres user, regardless of how the db is created. + if os.name == 'nt': set_permissions(db_name) + +def set_permissions(db_name): + """ + Sets the permissions on the given database to that of the user specified + in the settings. Needed specifically for PostGIS on Win32 platforms. + """ + cursor = connection.cursor() + user = settings.DATABASE_USER + cursor.execute('ALTER TABLE geometry_columns OWNER TO %s' % user) + cursor.execute('ALTER TABLE spatial_ref_sys OWNER TO %s' % user) diff --git a/webapp/django/contrib/gis/db/backend/postgis/field.py b/webapp/django/contrib/gis/db/backend/postgis/field.py new file mode 100644 index 0000000000..9d6c0fad24 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/field.py @@ -0,0 +1,95 @@ +from django.db import connection +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.db.backend.util import gqn +from django.contrib.gis.db.backend.postgis.query import TRANSFORM + +# Quotename & geographic quotename, respectively +qn = connection.ops.quote_name + +class PostGISField(Field): + """ + The backend-specific geographic field for PostGIS. + """ + + def _add_geom(self, style, db_table): + """ + Constructs the addition of the geometry to the table using the + AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. + + Takes the style object (provides syntax highlighting) and the + database table as parameters. + """ + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_TABLE('AddGeometryColumn') + '(' + \ + style.SQL_TABLE(gqn(db_table)) + ', ' + \ + style.SQL_FIELD(gqn(self.column)) + ', ' + \ + style.SQL_FIELD(str(self._srid)) + ', ' + \ + style.SQL_COLTYPE(gqn(self._geom)) + ', ' + \ + style.SQL_KEYWORD(str(self._dim)) + ');' + + if not self.null: + # Add a NOT NULL constraint to the field + sql += '\n' + \ + style.SQL_KEYWORD('ALTER TABLE ') + \ + style.SQL_TABLE(qn(db_table)) + \ + style.SQL_KEYWORD(' ALTER ') + \ + style.SQL_FIELD(qn(self.column)) + \ + style.SQL_KEYWORD(' SET NOT NULL') + ';' + return sql + + def _geom_index(self, style, db_table, + index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): + "Creates a GiST index for this geometry field." + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(qn(db_table)) + \ + style.SQL_KEYWORD(' USING ') + \ + style.SQL_COLTYPE(index_type) + ' ( ' + \ + style.SQL_FIELD(qn(self.column)) + ' ' + \ + style.SQL_KEYWORD(index_opts) + ' );' + return sql + + def post_create_sql(self, style, db_table): + """ + Returns SQL that will be executed after the model has been + created. Geometry columns must be added after creation with the + PostGIS AddGeometryColumn() function. + """ + + # Getting the AddGeometryColumn() SQL necessary to create a PostGIS + # geometry field. + post_sql = self._add_geom(style, db_table) + + # If the user wants to index this data, then get the indexing SQL as well. + if self._index: + return (post_sql, self._geom_index(style, db_table)) + else: + return (post_sql,) + + def _post_delete_sql(self, style, db_table): + "Drops the geometry column." + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ + style.SQL_TABLE(gqn(db_table)) + ', ' + \ + style.SQL_FIELD(gqn(self.column)) + ');' + return sql + + def db_type(self): + """ + PostGIS geometry columns are added by stored procedures, should be + None. + """ + return None + + def get_placeholder(self, value): + """ + Provides a proper substitution value for Geometries that are not in the + SRID of the field. Specifically, this routine will substitute in the + ST_Transform() function call. + """ + if value is None or value.srid == self._srid: + return '%s' + else: + # Adding Transform() to the SQL placeholder. + return '%s(%%s, %s)' % (TRANSFORM, self._srid) diff --git a/webapp/django/contrib/gis/db/backend/postgis/management.py b/webapp/django/contrib/gis/db/backend/postgis/management.py new file mode 100644 index 0000000000..c1cb32a04f --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/management.py @@ -0,0 +1,54 @@ +""" + This utility module is for obtaining information about the PostGIS + installation. + + See PostGIS docs at Ch. 6.2.1 for more information on these functions. +""" +import re + +def _get_postgis_func(func): + "Helper routine for calling PostGIS functions and returning their result." + from django.db import connection + cursor = connection.cursor() + cursor.execute('SELECT %s()' % func) + row = cursor.fetchone() + cursor.close() + return row[0] + +### PostGIS management functions ### +def postgis_geos_version(): + "Returns the version of the GEOS library used with PostGIS." + return _get_postgis_func('postgis_geos_version') + +def postgis_lib_version(): + "Returns the version number of the PostGIS library used with PostgreSQL." + return _get_postgis_func('postgis_lib_version') + +def postgis_proj_version(): + "Returns the version of the PROJ.4 library used with PostGIS." + return _get_postgis_func('postgis_proj_version') + +def postgis_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_version') + +def postgis_full_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_full_version') + +### Routines for parsing output of management functions. ### +version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') +def postgis_version_tuple(): + "Returns the PostGIS version as a tuple." + + # Getting the PostGIS version + version = postgis_lib_version() + m = version_regex.match(version) + if m: + major = int(m.group('major')) + minor1 = int(m.group('minor1')) + minor2 = int(m.group('minor2')) + else: + raise Exception('Could not parse PostGIS version string: %s' % version) + + return (version, major, minor1, minor2) diff --git a/webapp/django/contrib/gis/db/backend/postgis/models.py b/webapp/django/contrib/gis/db/backend/postgis/models.py new file mode 100644 index 0000000000..e032da4d89 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/models.py @@ -0,0 +1,58 @@ +""" + The GeometryColumns and SpatialRefSys models for the PostGIS backend. +""" +from django.db import models +from django.contrib.gis.models import SpatialRefSysMixin + +# Checking for the presence of GDAL (needed for the SpatialReference object) +from django.contrib.gis.gdal import HAS_GDAL +if HAS_GDAL: + from django.contrib.gis.gdal import SpatialReference + +class GeometryColumns(models.Model): + """ + The 'geometry_columns' table from the PostGIS. See the PostGIS + documentation at Ch. 4.2.2. + """ + f_table_catalog = models.CharField(max_length=256) + f_table_schema = models.CharField(max_length=256) + f_table_name = models.CharField(max_length=256) + f_geometry_column = models.CharField(max_length=256) + coord_dimension = models.IntegerField() + srid = models.IntegerField(primary_key=True) + type = models.CharField(max_length=30) + + class Meta: + db_table = 'geometry_columns' + + @classmethod + def table_name_col(cls): + "Class method for returning the table name column for this model." + return 'f_table_name' + + def __unicode__(self): + return "%s.%s - %dD %s field (SRID: %d)" % \ + (self.f_table_name, self.f_geometry_column, + self.coord_dimension, self.type, self.srid) + +class SpatialRefSys(models.Model, SpatialRefSysMixin): + """ + The 'spatial_ref_sys' table from PostGIS. See the PostGIS + documentaiton at Ch. 4.2.1. + """ + srid = models.IntegerField(primary_key=True) + auth_name = models.CharField(max_length=256) + auth_srid = models.IntegerField() + srtext = models.CharField(max_length=2048) + proj4text = models.CharField(max_length=2048) + + class Meta: + db_table = 'spatial_ref_sys' + + @property + def wkt(self): + return self.srtext + + @classmethod + def wkt_col(cls): + return 'srtext' diff --git a/webapp/django/contrib/gis/db/backend/postgis/query.py b/webapp/django/contrib/gis/db/backend/postgis/query.py new file mode 100644 index 0000000000..8780780402 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/postgis/query.py @@ -0,0 +1,287 @@ +""" + This module contains the spatial lookup types, and the get_geo_where_clause() + routine for PostGIS. +""" +import re +from decimal import Decimal +from django.db import connection +from django.contrib.gis.measure import Distance +from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple +from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction +qn = connection.ops.quote_name + +# Getting the PostGIS version information +POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple() + +# The supported PostGIS versions. +# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. +# Versions <= 1.0.x do not use GEOS C API, and will not be supported. +if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1): + raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION) + +# Versions of PostGIS >= 1.2.2 changed their naming convention to be +# 'SQL-MM-centric' to conform with the ISO standard. Practically, this +# means that 'ST_' prefixes geometry function names. +GEOM_FUNC_PREFIX = '' +if MAJOR_VERSION >= 1: + if (MINOR_VERSION1 > 2 or + (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)): + GEOM_FUNC_PREFIX = 'ST_' + + def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func) + + # Custom selection not needed for PostGIS because GEOS geometries are + # instantiated directly from the HEXEWKB returned by default. If + # WKT is needed for some reason in the future, this value may be changed, + # e.g,, 'AsText(%s)'. + GEOM_SELECT = None + + # Functions used by the GeoManager & GeoQuerySet + AREA = get_func('Area') + ASKML = get_func('AsKML') + ASGML = get_func('AsGML') + ASSVG = get_func('AsSVG') + CENTROID = get_func('Centroid') + DIFFERENCE = get_func('Difference') + DISTANCE = get_func('Distance') + DISTANCE_SPHERE = get_func('distance_sphere') + DISTANCE_SPHEROID = get_func('distance_spheroid') + ENVELOPE = get_func('Envelope') + EXTENT = get_func('extent') + GEOM_FROM_TEXT = get_func('GeomFromText') + GEOM_FROM_WKB = get_func('GeomFromWKB') + INTERSECTION = get_func('Intersection') + LENGTH = get_func('Length') + LENGTH_SPHEROID = get_func('length_spheroid') + MAKE_LINE = get_func('MakeLine') + MEM_SIZE = get_func('mem_size') + NUM_GEOM = get_func('NumGeometries') + NUM_POINTS = get_func('npoints') + PERIMETER = get_func('Perimeter') + POINT_ON_SURFACE = get_func('PointOnSurface') + SCALE = get_func('Scale') + SYM_DIFFERENCE = get_func('SymDifference') + TRANSFORM = get_func('Transform') + TRANSLATE = get_func('Translate') + + # Special cases for union and KML methods. + if MINOR_VERSION1 < 3: + UNIONAGG = 'GeomUnion' + UNION = 'Union' + else: + UNIONAGG = 'ST_Union' + UNION = 'ST_Union' + + if MINOR_VERSION1 == 1: + ASKML = False +else: + raise NotImplementedError('PostGIS versions < 1.0 are not supported.') + +#### Classes used in constructing PostGIS spatial SQL #### +class PostGISOperator(SpatialOperation): + "For PostGIS operators (e.g. `&&`, `~`)." + def __init__(self, operator): + super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s') + +class PostGISFunction(SpatialFunction): + "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)." + def __init__(self, function, **kwargs): + super(PostGISFunction, self).__init__(get_func(function), **kwargs) + +class PostGISFunctionParam(PostGISFunction): + "For PostGIS functions that take another parameter (e.g. DWithin, Relate)." + def __init__(self, func): + super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)') + +class PostGISDistance(PostGISFunction): + "For PostGIS distance operations." + dist_func = 'Distance' + def __init__(self, operator): + super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISSpheroidDistance(PostGISFunction): + "For PostGIS spherical distance operations (using the spheroid)." + dist_func = 'distance_spheroid' + def __init__(self, operator): + # An extra parameter in `end_subst` is needed for the spheroid string. + super(PostGISSpheroidDistance, self).__init__(self.dist_func, + beg_subst='%s(%s, %%s, %%s', + end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISSphereDistance(PostGISFunction): + "For PostGIS spherical distance operations." + dist_func = 'distance_sphere' + def __init__(self, operator): + super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s', + operator=operator, result='%%s') + +class PostGISRelate(PostGISFunctionParam): + "For PostGIS Relate(<geom>, <pattern>) calls." + pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, pattern): + if not self.pattern_regex.match(pattern): + raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) + super(PostGISRelate, self).__init__('Relate') + +#### Lookup type mapping dictionaries of PostGIS operations. #### + +# PostGIS-specific operators. The commented descriptions of these +# operators come from Section 6.2.2 of the official PostGIS documentation. +POSTGIS_OPERATORS = { + # The "&<" operator returns true if A's bounding box overlaps or + # is to the left of B's bounding box. + 'overlaps_left' : PostGISOperator('&<'), + # The "&>" operator returns true if A's bounding box overlaps or + # is to the right of B's bounding box. + 'overlaps_right' : PostGISOperator('&>'), + # The "<<" operator returns true if A's bounding box is strictly + # to the left of B's bounding box. + 'left' : PostGISOperator('<<'), + # The ">>" operator returns true if A's bounding box is strictly + # to the right of B's bounding box. + 'right' : PostGISOperator('>>'), + # The "&<|" operator returns true if A's bounding box overlaps or + # is below B's bounding box. + 'overlaps_below' : PostGISOperator('&<|'), + # The "|&>" operator returns true if A's bounding box overlaps or + # is above B's bounding box. + 'overlaps_above' : PostGISOperator('|&>'), + # The "<<|" operator returns true if A's bounding box is strictly + # below B's bounding box. + 'strictly_below' : PostGISOperator('<<|'), + # The "|>>" operator returns true if A's bounding box is strictly + # above B's bounding box. + 'strictly_above' : PostGISOperator('|>>'), + # The "~=" operator is the "same as" operator. It tests actual + # geometric equality of two features. So if A and B are the same feature, + # vertex-by-vertex, the operator returns true. + 'same_as' : PostGISOperator('~='), + 'exact' : PostGISOperator('~='), + # The "@" operator returns true if A's bounding box is completely contained + # by B's bounding box. + 'contained' : PostGISOperator('@'), + # The "~" operator returns true if A's bounding box completely contains + # by B's bounding box. + 'bbcontains' : PostGISOperator('~'), + # The "&&" operator returns true if A's bounding box overlaps + # B's bounding box. + 'bboverlaps' : PostGISOperator('&&'), + } + +# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query +# first before calling the more computationally expensive GEOS routines (called +# "inline index magic"): +# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and +# 'covers'. +POSTGIS_GEOMETRY_FUNCTIONS = { + 'equals' : PostGISFunction('Equals'), + 'disjoint' : PostGISFunction('Disjoint'), + 'touches' : PostGISFunction('Touches'), + 'crosses' : PostGISFunction('Crosses'), + 'within' : PostGISFunction('Within'), + 'overlaps' : PostGISFunction('Overlaps'), + 'contains' : PostGISFunction('Contains'), + 'intersects' : PostGISFunction('Intersects'), + 'relate' : (PostGISRelate, basestring), + } + +# Valid distance types and substitutions +dtypes = (Decimal, Distance, float, int, long) +def get_dist_ops(operator): + "Returns operations for both regular and spherical distances." + return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator)) +DISTANCE_FUNCTIONS = { + 'distance_gt' : (get_dist_ops('>'), dtypes), + 'distance_gte' : (get_dist_ops('>='), dtypes), + 'distance_lt' : (get_dist_ops('<'), dtypes), + 'distance_lte' : (get_dist_ops('<='), dtypes), + } + +if GEOM_FUNC_PREFIX == 'ST_': + # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ + POSTGIS_GEOMETRY_FUNCTIONS.update( + {'coveredby' : PostGISFunction('CoveredBy'), + 'covers' : PostGISFunction('Covers'), + }) + DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes) + +# Distance functions are a part of PostGIS geometry functions. +POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS) + +# Any other lookup types that do not require a mapping. +MISC_TERMS = ['isnull'] + +# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types +# allowed for geographic queries. +POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first +POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions +POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') +POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups + +# For checking tuple parameters -- not very pretty but gets job done. +def exactly_two(val): return val == 2 +def two_to_three(val): return val >= 2 and val <=3 +def num_params(lookup_type, val): + if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val) + else: return exactly_two(val) + +#### The `get_geo_where_clause` function for PostGIS. #### +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): + "Returns the SQL WHERE clause for use in PostGIS SQL construction." + # Getting the quoted field as `geo_col`. + geo_col = '%s.%s' % (qn(table_alias), qn(name)) + if lookup_type in POSTGIS_OPERATORS: + # See if a PostGIS operator matches the lookup type. + return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) + elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS: + # See if a PostGIS geometry function matches the lookup type. + tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] + + # Lookup types that are tuples take tuple arguments, e.g., 'relate' and + # distance lookups. + if isinstance(tmp, tuple): + # First element of tuple is the PostGISOperation instance, and the + # second element is either the type or a tuple of acceptable types + # that may passed in as further parameters for the lookup type. + op, arg_type = tmp + + # Ensuring that a tuple _value_ was passed in from the user + if not isinstance(geo_annot.value, (tuple, list)): + raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) + + # Number of valid tuple parameters depends on the lookup type. + nparams = len(geo_annot.value) + if not num_params(lookup_type, nparams): + raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) + + # Ensuring the argument type matches what we expect. + if not isinstance(geo_annot.value[1], arg_type): + raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) + + # For lookup type `relate`, the op instance is not yet created (has + # to be instantiated here to check the pattern parameter). + if lookup_type == 'relate': + op = op(geo_annot.value[1]) + elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': + if geo_annot.geodetic: + # Geodetic distances are only availble from Points to PointFields. + if geo_annot.geom_type != 'POINT': + raise TypeError('PostGIS spherical operations are only valid on PointFields.') + if geo_annot.value[0].geom_typeid != 0: + raise TypeError('PostGIS geometry distance parameter is required to be of type Point.') + # Setting up the geodetic operation appropriately. + if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2] + else: op = op[1] + else: + op = op[0] + else: + op = tmp + # Calling the `as_sql` function on the operation instance. + return op.as_sql(geo_col) + elif lookup_type == 'isnull': + # Handling 'isnull' lookup type + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) + + raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/webapp/django/contrib/gis/db/backend/util.py b/webapp/django/contrib/gis/db/backend/util.py new file mode 100644 index 0000000000..a19dd975c1 --- /dev/null +++ b/webapp/django/contrib/gis/db/backend/util.py @@ -0,0 +1,52 @@ +from types import UnicodeType + +def gqn(val): + """ + The geographic quote name function; used for quoting tables and + geometries (they use single rather than the double quotes of the + backend quotename function). + """ + if isinstance(val, basestring): + if isinstance(val, UnicodeType): val = val.encode('ascii') + return "'%s'" % val + else: + return str(val) + +class SpatialOperation(object): + """ + Base class for generating spatial SQL. + """ + def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''): + self.function = function + self.operator = operator + self.result = result + self.beg_subst = beg_subst + try: + # Try and put the operator and result into to the + # end substitution. + self.end_subst = end_subst % (operator, result) + except TypeError: + self.end_subst = end_subst + + @property + def sql_subst(self): + return ''.join([self.beg_subst, self.end_subst]) + + def as_sql(self, geo_col): + return self.sql_subst % self.params(geo_col) + + def params(self, geo_col): + return (geo_col, self.operator) + +class SpatialFunction(SpatialOperation): + """ + Base class for generating spatial SQL related to a function. + """ + def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''): + # Getting the function prefix. + kwargs = {'function' : func, 'operator' : operator, 'result' : result, + 'beg_subst' : beg_subst, 'end_subst' : end_subst,} + super(SpatialFunction, self).__init__(**kwargs) + + def params(self, geo_col): + return (self.function, geo_col) diff --git a/webapp/django/contrib/gis/db/models/__init__.py b/webapp/django/contrib/gis/db/models/__init__.py new file mode 100644 index 0000000000..02a2c5318e --- /dev/null +++ b/webapp/django/contrib/gis/db/models/__init__.py @@ -0,0 +1,17 @@ +# Want to get everything from the 'normal' models package. +from django.db.models import * + +# The GeoManager +from django.contrib.gis.db.models.manager import GeoManager + +# The GeoQ object +from django.contrib.gis.db.models.query import GeoQ + +# The geographic-enabled fields. +from django.contrib.gis.db.models.fields import \ + GeometryField, PointField, LineStringField, PolygonField, \ + MultiPointField, MultiLineStringField, MultiPolygonField, \ + GeometryCollectionField + +# The geographic mixin class. +from mixin import GeoMixin diff --git a/webapp/django/contrib/gis/db/models/fields/__init__.py b/webapp/django/contrib/gis/db/models/fields/__init__.py new file mode 100644 index 0000000000..a1dfa23eb4 --- /dev/null +++ b/webapp/django/contrib/gis/db/models/fields/__init__.py @@ -0,0 +1,211 @@ +from django.contrib.gis import forms +# Getting the SpatialBackend container and the geographic quoting method. +from django.contrib.gis.db.backend import SpatialBackend, gqn +# GeometryProxy, GEOS, and Distance imports. +from django.contrib.gis.db.models.proxy import GeometryProxy +from django.contrib.gis.measure import Distance +# The `get_srid_info` function gets SRID information from the spatial +# reference system table w/o using the ORM. +from django.contrib.gis.models import get_srid_info + +#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. +class GeometryField(SpatialBackend.Field): + "The base GIS field -- maps to the OpenGIS Specification Geometry type." + + # The OpenGIS Geometry name. + _geom = 'GEOMETRY' + + # Geodetic units. + geodetic_units = ('Decimal Degree', 'degree') + + def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, **kwargs): + """ + The initialization function for geometry fields. Takes the following + as keyword arguments: + + srid: + The spatial reference system identifier, an OGC standard. + Defaults to 4326 (WGS84). + + spatial_index: + Indicates whether to create a spatial index. Defaults to True. + Set this instead of 'db_index' for geographic fields since index + creation is different for geometry columns. + + dim: + The number of dimensions for this geometry. Defaults to 2. + """ + + # Setting the index flag with the value of the `spatial_index` keyword. + self._index = spatial_index + + # Setting the SRID and getting the units. Unit information must be + # easily available in the field instance for distance queries. + self._srid = srid + self._unit, self._unit_name, self._spheroid = get_srid_info(srid) + + # Setting the dimension of the geometry field. + self._dim = dim + + # Setting the verbose_name keyword argument with the positional + # first parameter, so this works like normal fields. + kwargs['verbose_name'] = verbose_name + + super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function + + ### Routines specific to GeometryField ### + @property + def geodetic(self): + """ + Returns true if this field's SRID corresponds with a coordinate + system that uses non-projected units (e.g., latitude/longitude). + """ + return self._unit_name in self.geodetic_units + + def get_distance(self, dist_val, lookup_type): + """ + Returns a distance number in units of the field. For example, if + `D(km=1)` was passed in and the units of the field were in meters, + then 1000 would be returned. + """ + # Getting the distance parameter and any options. + if len(dist_val) == 1: dist, option = dist_val[0], None + else: dist, option = dist_val + + if isinstance(dist, Distance): + if self.geodetic: + # Won't allow Distance objects w/DWithin lookups on PostGIS. + if SpatialBackend.postgis and lookup_type == 'dwithin': + raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.') + # Spherical distance calculation parameter should be in meters. + dist_param = dist.m + else: + dist_param = getattr(dist, Distance.unit_attname(self._unit_name)) + else: + # Assuming the distance is in the units of the field. + dist_param = dist + + if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid': + # On PostGIS, by default `ST_distance_sphere` is used; but if the + # accuracy of `ST_distance_spheroid` is needed than the spheroid + # needs to be passed to the SQL stored procedure. + return [gqn(self._spheroid), dist_param] + else: + return [dist_param] + + def get_geometry(self, value): + """ + Retrieves the geometry, setting the default SRID from the given + lookup parameters. + """ + if isinstance(value, (tuple, list)): + geom = value[0] + else: + geom = value + + # When the input is not a GEOS geometry, attempt to construct one + # from the given string input. + if isinstance(geom, SpatialBackend.Geometry): + pass + elif isinstance(geom, basestring): + try: + geom = SpatialBackend.Geometry(geom) + except SpatialBackend.GeometryException: + raise ValueError('Could not create geometry from lookup value: %s' % str(value)) + else: + raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value)) + + # Assigning the SRID value. + geom.srid = self.get_srid(geom) + + return geom + + def get_srid(self, geom): + """ + Returns the default SRID for the given geometry, taking into account + the SRID set for the field. For example, if the input geometry + has no SRID, then that of the field will be returned. + """ + gsrid = geom.srid # SRID of given geometry. + if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1): + return self._srid + else: + return gsrid + + ### Routines overloaded from Field ### + def contribute_to_class(self, cls, name): + super(GeometryField, self).contribute_to_class(cls, name) + + # Setup for lazy-instantiated Geometry object. + setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self)) + + def formfield(self, **kwargs): + defaults = {'form_class' : forms.GeometryField, + 'geom_type' : self._geom, + 'null' : self.null, + } + defaults.update(kwargs) + return super(GeometryField, self).formfield(**defaults) + + def get_db_prep_lookup(self, lookup_type, value): + """ + Returns the spatial WHERE clause and associated parameters for the + given lookup type and value. The value will be prepared for database + lookup (e.g., spatial transformation SQL will be added if necessary). + """ + if lookup_type in SpatialBackend.gis_terms: + # special case for isnull lookup + if lookup_type == 'isnull': return [], [] + + # Get the geometry with SRID; defaults SRID to that of the field + # if it is None. + geom = self.get_geometry(value) + + # Getting the WHERE clause list and the associated params list. The params + # list is populated with the Adaptor wrapping the Geometry for the + # backend. The WHERE clause list contains the placeholder for the adaptor + # (e.g. any transformation SQL). + where = [self.get_placeholder(geom)] + params = [SpatialBackend.Adaptor(geom)] + + if isinstance(value, (tuple, list)): + if lookup_type in SpatialBackend.distance_functions: + # Getting the distance parameter in the units of the field. + where += self.get_distance(value[1:], lookup_type) + elif lookup_type in SpatialBackend.limited_where: + pass + else: + # Otherwise, making sure any other parameters are properly quoted. + where += map(gqn, value[1:]) + return where, params + else: + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def get_db_prep_save(self, value): + "Prepares the value for saving in the database." + if value is None: + return None + else: + return SpatialBackend.Adaptor(self.get_geometry(value)) + +# The OpenGIS Geometry Type Fields +class PointField(GeometryField): + _geom = 'POINT' + +class LineStringField(GeometryField): + _geom = 'LINESTRING' + +class PolygonField(GeometryField): + _geom = 'POLYGON' + +class MultiPointField(GeometryField): + _geom = 'MULTIPOINT' + +class MultiLineStringField(GeometryField): + _geom = 'MULTILINESTRING' + +class MultiPolygonField(GeometryField): + _geom = 'MULTIPOLYGON' + +class GeometryCollectionField(GeometryField): + _geom = 'GEOMETRYCOLLECTION' diff --git a/webapp/django/contrib/gis/db/models/manager.py b/webapp/django/contrib/gis/db/models/manager.py new file mode 100644 index 0000000000..602d11251a --- /dev/null +++ b/webapp/django/contrib/gis/db/models/manager.py @@ -0,0 +1,82 @@ +from django.db.models.manager import Manager +from django.contrib.gis.db.models.query import GeoQuerySet + +class GeoManager(Manager): + "Overrides Manager to return Geographic QuerySets." + + # This manager should be used for queries on related fields + # so that geometry columns on Oracle and MySQL are selected + # properly. + use_for_related_fields = True + + def get_query_set(self): + return GeoQuerySet(model=self.model) + + def area(self, *args, **kwargs): + return self.get_query_set().area(*args, **kwargs) + + def centroid(self, *args, **kwargs): + return self.get_query_set().centroid(*args, **kwargs) + + def difference(self, *args, **kwargs): + return self.get_query_set().difference(*args, **kwargs) + + def distance(self, *args, **kwargs): + return self.get_query_set().distance(*args, **kwargs) + + def envelope(self, *args, **kwargs): + return self.get_query_set().envelope(*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) + + def intersection(self, *args, **kwargs): + return self.get_query_set().intersection(*args, **kwargs) + + def kml(self, *args, **kwargs): + return self.get_query_set().kml(*args, **kwargs) + + def length(self, *args, **kwargs): + return self.get_query_set().length(*args, **kwargs) + + def make_line(self, *args, **kwargs): + return self.get_query_set().make_line(*args, **kwargs) + + def mem_size(self, *args, **kwargs): + return self.get_query_set().mem_size(*args, **kwargs) + + def num_geom(self, *args, **kwargs): + return self.get_query_set().num_geom(*args, **kwargs) + + def num_points(self, *args, **kwargs): + return self.get_query_set().num_points(*args, **kwargs) + + def perimeter(self, *args, **kwargs): + return self.get_query_set().perimeter(*args, **kwargs) + + def point_on_surface(self, *args, **kwargs): + return self.get_query_set().point_on_surface(*args, **kwargs) + + def scale(self, *args, **kwargs): + return self.get_query_set().scale(*args, **kwargs) + + def svg(self, *args, **kwargs): + return self.get_query_set().svg(*args, **kwargs) + + def sym_difference(self, *args, **kwargs): + return self.get_query_set().sym_difference(*args, **kwargs) + + def transform(self, *args, **kwargs): + return self.get_query_set().transform(*args, **kwargs) + + def translate(self, *args, **kwargs): + return self.get_query_set().translate(*args, **kwargs) + + def union(self, *args, **kwargs): + return self.get_query_set().union(*args, **kwargs) + + def unionagg(self, *args, **kwargs): + return self.get_query_set().unionagg(*args, **kwargs) diff --git a/webapp/django/contrib/gis/db/models/mixin.py b/webapp/django/contrib/gis/db/models/mixin.py new file mode 100644 index 0000000000..475a053b8f --- /dev/null +++ b/webapp/django/contrib/gis/db/models/mixin.py @@ -0,0 +1,11 @@ +# Until model subclassing is a possibility, a mixin class is used to add +# the necessary functions that may be contributed for geographic objects. +class GeoMixin: + """ + The Geographic Mixin class provides routines for geographic objects, + however, it is no longer necessary, since all of its previous functions + may now be accessed via the GeometryProxy. This mixin is only provided + for backwards-compatibility purposes, and will be eventually removed + (unless the need arises again). + """ + pass diff --git a/webapp/django/contrib/gis/db/models/proxy.py b/webapp/django/contrib/gis/db/models/proxy.py new file mode 100644 index 0000000000..34276a6d63 --- /dev/null +++ b/webapp/django/contrib/gis/db/models/proxy.py @@ -0,0 +1,62 @@ +""" + The GeometryProxy object, allows for lazy-geometries. The proxy uses + Python descriptors for instantiating and setting Geometry objects + corresponding to geographic model fields. + + Thanks to Robert Coup for providing this functionality (see #4322). +""" + +from types import NoneType, StringType, UnicodeType + +class GeometryProxy(object): + def __init__(self, klass, field): + """ + Proxy initializes on the given Geometry class (not an instance) and + the GeometryField. + """ + self._field = field + self._klass = klass + + def __get__(self, obj, type=None): + """ + This accessor retrieves the geometry, initializing it using the geometry + class specified during initialization and the HEXEWKB value of the field. + Currently, only GEOS or OGR geometries are supported. + """ + # Getting the value of the field. + geom_value = obj.__dict__[self._field.attname] + + if isinstance(geom_value, self._klass): + geom = geom_value + elif (geom_value is None) or (geom_value==''): + geom = None + else: + # Otherwise, a Geometry object is built using the field's contents, + # and the model's corresponding attribute is set. + geom = self._klass(geom_value) + setattr(obj, self._field.attname, geom) + return geom + + def __set__(self, obj, value): + """ + This accessor sets the proxied geometry with the geometry class + specified during initialization. Values of None, HEXEWKB, or WKT may + be used to set the geometry as well. + """ + # The OGC Geometry type of the field. + gtype = self._field._geom + + # The geometry type must match that of the field -- unless the + # general GeometryField is used. + if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): + # Assigning the SRID to the geometry. + if value.srid is None: value.srid = self._field._srid + elif isinstance(value, (NoneType, StringType, UnicodeType)): + # Set with None, WKT, or HEX + pass + else: + raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value))) + + # Setting the objects dictionary with the value, and returning. + obj.__dict__[self._field.attname] = value + return value diff --git a/webapp/django/contrib/gis/db/models/query.py b/webapp/django/contrib/gis/db/models/query.py new file mode 100644 index 0000000000..8efc720333 --- /dev/null +++ b/webapp/django/contrib/gis/db/models/query.py @@ -0,0 +1,617 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db import connection +from django.db.models.query import sql, QuerySet, Q + +from django.contrib.gis.db.backend import SpatialBackend +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.measure import Area, Distance +from django.contrib.gis.models import get_srid_info +qn = connection.ops.quote_name + +# For backwards-compatibility; Q object should work just fine +# after queryset-refactor. +class GeoQ(Q): pass + +class GeomSQL(object): + "Simple wrapper object for geometric SQL." + def __init__(self, geo_sql): + self.sql = geo_sql + + def as_sql(self, *args, **kwargs): + return self.sql + +class GeoQuerySet(QuerySet): + "The Geographic 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 area(self, tolerance=0.05, **kwargs): + """ + Returns the area of the geographic field in an `area` attribute on + each element of this GeoQuerySet. + """ + # Peforming setup here rather than in `_spatial_attribute` so that + # we can get the units for `AreaField`. + procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None)) + s = {'procedure_args' : procedure_args, + 'geo_field' : geo_field, + 'setup' : False, + } + if SpatialBackend.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: + if not geo_field.geodetic: + # Getting the area units of the geographic field. + s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name)) + else: + # TODO: Do we want to support raw number areas for geodetic fields? + raise Exception('Area on geodetic coordinate systems not supported.') + return self._spatial_attribute('area', s, **kwargs) + + def centroid(self, **kwargs): + """ + Returns the centroid of the geographic field in a `centroid` + attribute on each element of this GeoQuerySet. + """ + return self._geom_attribute('centroid', **kwargs) + + def difference(self, geom, **kwargs): + """ + Returns the spatial difference of the geographic field in a `difference` + attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('difference', geom, **kwargs) + + def distance(self, geom, **kwargs): + """ + Returns the distance from the given geographic field name to the + given geometry in a `distance` attribute on each element of the + GeoQuerySet. + + Keyword Arguments: + `spheroid` => If the geometry field is geodetic and PostGIS is + the spatial database, then the more accurate + spheroid calculation will be used instead of the + quicker sphere calculation. + + `tolerance` => Used only for Oracle. The tolerance is + in meters -- a default of 5 centimeters (0.05) + is used. + """ + return self._distance_attribute('distance', geom, **kwargs) + + def envelope(self, **kwargs): + """ + Returns a Geometry representing the bounding box of the + Geometry field in an `envelope` attribute on each element of + the GeoQuerySet. + """ + return self._geom_attribute('envelope', **kwargs) + + def extent(self, **kwargs): + """ + 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). + """ + convert_extent = None + if SpatialBackend.postgis: + def convert_extent(box, geo_field): + # 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) + elif SpatialBackend.oracle: + def convert_extent(wkt, geo_field): + raise NotImplementedError + return self._spatial_aggregate('extent', convert_func=convert_extent, **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. + """ + s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} + if SpatialBackend.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)): + procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' + else: + procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' + s['procedure_args'] = {'precision' : precision, 'version' : version} + + return self._spatial_attribute('gml', s, **kwargs) + + def intersection(self, geom, **kwargs): + """ + Returns the spatial intersection of the Geometry field in + an `intersection` attribute on each element of this + GeoQuerySet. + """ + return self._geomset_attribute('intersection', geom, **kwargs) + + def kml(self, **kwargs): + """ + Returns KML representation of the geometry field in a `kml` + attribute on each element of this GeoQuerySet. + """ + s = {'desc' : 'KML', + 'procedure_fmt' : '%(geo_col)s,%(precision)s', + 'procedure_args' : {'precision' : kwargs.pop('precision', 8)}, + } + return self._spatial_attribute('kml', s, **kwargs) + + def length(self, **kwargs): + """ + Returns the length of the geometry field as a `Distance` object + stored in a `length` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('length', None, **kwargs) + + def make_line(self, **kwargs): + """ + Creates a linestring from all of the PointField geometries in the + this GeoQuerySet and returns it. This is a spatial aggregate + method, and thus returns a geometry rather than a GeoQuerySet. + """ + kwargs['geo_field_type'] = PointField + kwargs['agg_field'] = GeometryField + return self._spatial_aggregate('make_line', **kwargs) + + def mem_size(self, **kwargs): + """ + Returns the memory size (number of bytes) that the geometry field takes + in a `mem_size` attribute on each element of this GeoQuerySet. + """ + return self._spatial_attribute('mem_size', {}, **kwargs) + + def num_geom(self, **kwargs): + """ + Returns the number of geometries if the field is a + GeometryCollection or Multi* Field in a `num_geom` + attribute on each element of this GeoQuerySet; otherwise + the sets with None. + """ + return self._spatial_attribute('num_geom', {}, **kwargs) + + def num_points(self, **kwargs): + """ + Returns the number of points in the first linestring in the + Geometry field in a `num_points` attribute on each element of + this GeoQuerySet; otherwise sets with None. + """ + return self._spatial_attribute('num_points', {}, **kwargs) + + def perimeter(self, **kwargs): + """ + Returns the perimeter of the geometry field as a `Distance` object + stored in a `perimeter` attribute on each element of this GeoQuerySet. + """ + return self._distance_attribute('perimeter', None, **kwargs) + + def point_on_surface(self, **kwargs): + """ + Returns a Point geometry guaranteed to lie on the surface of the + Geometry field in a `point_on_surface` attribute on each element + of this GeoQuerySet; otherwise sets with None. + """ + return self._geom_attribute('point_on_surface', **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. + """ + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('scale', s, **kwargs) + + def svg(self, **kwargs): + """ + Returns SVG representation of the geographic field in a `svg` + attribute on each element of this GeoQuerySet. + """ + s = {'desc' : 'SVG', + 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', + 'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)), + 'precision' : kwargs.pop('precision', 8)}, + } + return self._spatial_attribute('svg', s, **kwargs) + + def sym_difference(self, geom, **kwargs): + """ + Returns the symmetric difference of the geographic field in a + `sym_difference` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('sym_difference', geom, **kwargs) + + def translate(self, x, y, z=0.0, **kwargs): + """ + Translates the geometry to a new location using the given numeric + parameters as offsets. + """ + s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', + 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, + 'select_field' : GeomField(), + } + return self._spatial_attribute('translate', s, **kwargs) + + def transform(self, srid=4326, **kwargs): + """ + Transforms the given geometry field to the given SRID. If no SRID is + provided, the transformation will default to using 4326 (WGS84). + """ + if not isinstance(srid, (int, long)): + raise TypeError('An integer SRID must be provided.') + field_name = kwargs.get('field_name', None) + tmp, geo_field = self._spatial_setup('transform', field_name=field_name) + + # Getting the selection SQL for the given geographic field. + field_col = self._geocol_select(geo_field, field_name) + + # Why cascading substitutions? Because spatial backends like + # Oracle and MySQL already require a function call to convert to text, thus + # when there's also a transformation we need to cascade the substitutions. + # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' + geo_col = self.query.custom_select.get(geo_field, field_col) + + # 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) + # 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 + self.query.custom_select[geo_field] = custom_sel + return self._clone() + + def union(self, geom, **kwargs): + """ + Returns the union of the geographic field with the given + Geometry in a `union` attribute on each element of this GeoQuerySet. + """ + return self._geomset_attribute('union', geom, **kwargs) + + def unionagg(self, **kwargs): + """ + Performs an aggregate union on the given geometry field. Returns + None if the GeoQuerySet is empty. The `tolerance` keyword is for + Oracle backends only. + """ + kwargs['agg_field'] = GeometryField + return self._spatial_aggregate('unionagg', **kwargs) + + ### Private API -- Abstracted DRY routines. ### + def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None): + """ + Performs set up for executing the spatial function. + """ + # Does the spatial backend support this? + func = getattr(SpatialBackend, att, False) + if desc is None: desc = att + if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc) + + # Initializing the procedure arguments. + procedure_args = {'function' : func} + + # Is there a geographic field in the model to perform this + # operation on? + geo_field = self.query._geo_field(field_name) + if not geo_field: + raise TypeError('%s output only available on GeometryFields.' % func) + + # If the `geo_field_type` keyword was used, then enforce that + # type limitation. + if not geo_field_type is None and not isinstance(geo_field, geo_field_type): + raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) + + # Setting the procedure args. + procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate) + + return procedure_args, geo_field + + def _spatial_aggregate(self, att, field_name=None, + agg_field=None, convert_func=None, + geo_field_type=None, tolerance=0.0005): + """ + DRY routine for calling aggregate spatial stored procedures and + returning their result to the caller of the function. + """ + # Constructing the setup keyword arguments. + setup_kwargs = {'aggregate' : True, + 'field_name' : field_name, + 'geo_field_type' : geo_field_type, + } + procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs) + + if SpatialBackend.oracle: + procedure_args['tolerance'] = tolerance + # Adding in selection SQL for Oracle geometry columns. + if agg_field is GeometryField: + agg_sql = '%s' % SpatialBackend.select + else: + agg_sql = '%s' + agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args) + else: + agg_sql = '%(function)s(%(geo_col)s)' % procedure_args + + # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and + # specifying the type of the aggregate field. + self.query.select = [GeomSQL(agg_sql)] + self.query.select_fields = [agg_field] + + try: + # `asql` => not overriding `sql` module. + asql, params = self.query.as_sql() + except sql.datastructures.EmptyResultSet: + return None + + # Getting a cursor, executing the query, and extracting the returned + # value from the aggregate function. + cursor = connection.cursor() + cursor.execute(asql, params) + result = cursor.fetchone()[0] + + # If the `agg_field` is specified as a GeometryField, then autmatically + # set up the conversion function. + if agg_field is GeometryField and not callable(convert_func): + if SpatialBackend.postgis: + def convert_geom(hex, geo_field): + if hex: return SpatialBackend.Geometry(hex) + else: return None + elif SpatialBackend.oracle: + def convert_geom(clob, geo_field): + if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) + else: return None + convert_func = convert_geom + + # Returning the callback function evaluated on the result culled + # from the executed cursor. + if callable(convert_func): + return convert_func(result, geo_field) + else: + return result + + def _spatial_attribute(self, att, settings, field_name=None, model_att=None): + """ + DRY routine for calling a spatial stored procedure on a geometry column + and attaching its output as an attribute of the model. + + Arguments: + att: + The name of the spatial attribute that holds the spatial + SQL function to call. + + settings: + Dictonary of internal settings to customize for the spatial procedure. + + Public Keyword Arguments: + + field_name: + The name of the geographic field to call the spatial + function on. May also be a lookup to a geometry field + as part of a foreign key relation. + + model_att: + The name of the model attribute to attach the output of + the spatial function to. + """ + # Default settings. + settings.setdefault('desc', None) + settings.setdefault('geom_args', ()) + settings.setdefault('geom_field', None) + settings.setdefault('procedure_args', {}) + settings.setdefault('procedure_fmt', '%(geo_col)s') + settings.setdefault('select_params', []) + + # 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) + for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) + else: + geo_field = settings['geo_field'] + + # The attribute to attach to the model. + if not isinstance(model_att, basestring): model_att = att + + # 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]) + # Replacing the procedure format with that of any needed + # transformation SQL. + old_fmt = '%%(%s)s' % name + new_fmt = where[0] % '%%s' + settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt) + settings['select_params'].extend(params) + + # Getting the format for the stored procedure. + fmt = '%%(function)s(%s)' % settings['procedure_fmt'] + + # 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 + self.query.extra_select_fields[model_att] = sel_fld + + # Finally, setting the extra selection attribute with + # the format string expanded with the stored procedure + # arguments. + return self.extra(select={model_att : fmt % settings['procedure_args']}, + select_params=settings['select_params']) + + def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): + """ + DRY routine for GeoQuerySet distance attribute routines. + """ + # Setting up the distance procedure arguments. + procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None)) + + # 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: + dist_att = 'm' + else: + dist_att = Distance.unit_attname(geo_field._unit_name) + + # Shortcut booleans for what distance function we're using. + distance = func == 'distance' + length = func == 'length' + perimeter = func == 'perimeter' + if not (distance or length or perimeter): + raise ValueError('Unknown distance function: %s' % func) + + # 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] + + # If the spheroid calculation is desired, either by the `spheroid` + # keyword or wehn 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): + lookup_params.append('spheroid') + where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params) + + # The `geom_args` flag is set to true if a geometry parameter was + # passed in. + geom_args = bool(geom) + + if SpatialBackend.oracle: + if distance: + procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s' + elif length or perimeter: + procedure_fmt = '%(geo_col)s,%(tolerance)s' + procedure_args['tolerance'] = tolerance + else: + # 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) + geodetic = unit_name in geo_field.geodetic_units + else: + geodetic = geo_field.geodetic + + if distance: + if self.query.transformed_srid: + # Setting the `geom_args` flag to false because we want to handle + # transformation SQL here, rather than the way done by default + # (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) + 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. + procedure_fmt += ', %%s' + else: + # We need to transform the geom to the srid specified in `transform()`, + # so wrapping the geometry placeholder in transformation SQL. + procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) + else: + # `transform()` was not used on this GeoQuerySet. + procedure_fmt = '%(geo_col)s,%(geom)s' + + if 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 TypeError('Spherical distance calculation only supported on PointFields.') + if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': + raise TypeError('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]}) + else: + procedure_args.update({'function' : SpatialBackend.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]}) + + # Setting up the settings for `_spatial_attribute`. + s = {'select_field' : DistanceField(dist_att), + 'setup' : False, + 'geo_field' : geo_field, + 'procedure_args' : procedure_args, + 'procedure_fmt' : procedure_fmt, + } + if geom_args: + s['geom_args'] = ('geom',) + s['procedure_args']['geom'] = geom + elif geom: + # The geometry is passed in as a parameter because we handled + # transformation conditions in this routine. + s['select_params'] = [SpatialBackend.Adaptor(geom)] + return self._spatial_attribute(func, s, **kwargs) + + def _geom_attribute(self, func, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute (e.g., `centroid`, `point_on_surface`). + """ + s = {'select_field' : GeomField(),} + if SpatialBackend.oracle: + s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' + s['procedure_args'] = {'tolerance' : tolerance} + return self._spatial_attribute(func, s, **kwargs) + + def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): + """ + DRY routine for setting up a GeoQuerySet method that attaches a + Geometry attribute and takes a Geoemtry parameter. This is used + for geometry set-like operations (e.g., intersection, difference, + union, sym_difference). + """ + s = {'geom_args' : ('geom',), + 'select_field' : GeomField(), + 'procedure_fmt' : '%(geo_col)s,%(geom)s', + 'procedure_args' : {'geom' : geom}, + } + if SpatialBackend.oracle: + s['procedure_fmt'] += ',%(tolerance)s' + s['procedure_args']['tolerance'] = tolerance + return self._spatial_attribute(func, s, **kwargs) + + def _geocol_select(self, geo_field, field_name, aggregate=False): + """ + Helper routine for constructing the SQL to select the geographic + column. Takes into account if the geographic field is in a + ForeignKey relation to the current model. + """ + # If this is an aggregate spatial query, the flag needs to be + # set on the `GeoQuery` object of this queryset. + if aggregate: self.query.aggregate = True + + # Is this operation going to be on a related geographic field? + if not geo_field in self.model._meta.fields: + # 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() + 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) + else: + return self.query._field_column(geo_field) diff --git a/webapp/django/contrib/gis/db/models/sql/__init__.py b/webapp/django/contrib/gis/db/models/sql/__init__.py new file mode 100644 index 0000000000..4a66b41664 --- /dev/null +++ b/webapp/django/contrib/gis/db/models/sql/__init__.py @@ -0,0 +1,2 @@ +from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery +from django.contrib.gis.db.models.sql.where import GeoWhereNode diff --git a/webapp/django/contrib/gis/db/models/sql/query.py b/webapp/django/contrib/gis/db/models/sql/query.py new file mode 100644 index 0000000000..f3e9fb25ca --- /dev/null +++ b/webapp/django/contrib/gis/db/models/sql/query.py @@ -0,0 +1,327 @@ +from itertools import izip +from django.db.models.query import sql +from django.db.models.fields import FieldDoesNotExist +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.where import GeoWhereNode +from django.contrib.gis.measure import Area, Distance + +# Valid GIS query types. +ALL_TERMS = sql.constants.QUERY_TERMS.copy() +ALL_TERMS.update(SpatialBackend.gis_terms) + +class GeoQuery(sql.Query): + """ + A single spatial SQL query. + """ + # Overridding the valid query terms. + query_terms = ALL_TERMS + + #### Methods overridden from the base Query class #### + def __init__(self, model, conn): + super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) + # 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 = {} + + def clone(self, *args, **kwargs): + 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() + return obj + + def get_columns(self, with_aliases=False): + """ + Return the list of columns to use in the select statement. If no + columns have been specified, returns all columns relating to fields in + the model. + + If 'with_aliases' is true, any column names that are duplicated + (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 + 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)) + for alias, col in self.extra_select.iteritems()] + aliases = set(self.extra_select.keys()) + if with_aliases: + col_aliases = aliases.copy() + else: + col_aliases = set() + if self.select: + # This loop customized for GeoQuery. + 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) + 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) + # 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) + + self._select_aliases = aliases + return result + + def get_default_columns(self, with_aliases=False, col_aliases=None, + start_alias=None, opts=None, as_pairs=False): + """ + Computes the default columns for selecting every field in the base + model. + + 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 + geometry columns. + """ + result = [] + if opts is None: + opts = self.model._meta + if start_alias: + table_alias = start_alias + else: + table_alias = self.tables[0] + root_pk = self.model._meta.pk.column + seen = {None: table_alias} + aliases = set() + for field, model in opts.get_fields_with_model(): + try: + alias = seen[model] + except KeyError: + alias = self.join((table_alias, model._meta.db_table, + root_pk, model._meta.pk.column)) + seen[model] = alias + if as_pairs: + result.append((alias, field.column)) + continue + # This part of the function is customized for GeoQuery. We + # see if there was any custom selection specified in the + # dictionary, and set up the selection format appropriately. + field_sel = self.get_field_select(field, alias) + if with_aliases and field.column in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (field_sel, c_alias)) + col_aliases.add(c_alias) + aliases.add(c_alias) + else: + r = field_sel + result.append(r) + aliases.add(r) + if with_aliases: + col_aliases.add(field.column) + if as_pairs: + 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 + objects. + """ + values = [] + aliases = self.extra_select.keys() + index_start = len(aliases) + values = [self.convert_values(v, self.extra_select_fields.get(a, None)) + for v, a in izip(row[:index_start], aliases)] + if SpatialBackend.oracle: + # This is what happens normally in Oracle's `resolve_columns`. + for value, field in izip(row[index_start:], fields): + values.append(self.convert_values(value, field)) + else: + values.extend(row[index_start:]) + return values + + def convert_values(self, value, field): + """ + Using the same routines that Oracle does we can convert our + extra selection objects into Geometry and Distance objects. + TODO: Laziness. + """ + if SpatialBackend.oracle: + # Running through Oracle's first. + value = super(GeoQuery, self).convert_values(value, field) + if isinstance(field, DistanceField): + # Using the field's distance attribute, can instantiate + # `Distance` with the right context. + value = Distance(**{field.distance_att : value}) + elif isinstance(field, AreaField): + value = Area(**{field.area_att : value}) + elif isinstance(field, GeomField): + value = SpatialBackend.Geometry(value) + return value + + #### Routines unique to GeoQuery #### + def get_extra_select_format(self, alias): + sel_fmt = '%s' + if alias in self.custom_select: + sel_fmt = sel_fmt % self.custom_select[alias] + return sel_fmt + + 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 + `GeoQuery`. + """ + sel_fmt = self.get_select_format(fld) + if fld in self.custom_select: + field_sel = sel_fmt % self.custom_select[fld] + else: + field_sel = sel_fmt % self._field_column(fld, alias) + return field_sel + + def get_select_format(self, fld): + """ + Returns the selection format string, depending on the requirements + of the spatial backend. For example, Oracle and MySQL require custom + selection formats in order to retrieve geometries in OGC WKT. For all + other fields a simple '%s' format string is returned. + """ + if SpatialBackend.select and hasattr(fld, '_geom'): + # This allows operations to be done on fields in the SELECT, + # overriding their values -- used by the Oracle and MySQL + # spatial backends to get database values as WKT, and by the + # `transform` method. + sel_fmt = SpatialBackend.select + + # Because WKT doesn't contain spatial reference information, + # the SRID is prefixed to the returned WKT to ensure that the + # transformed geometries have an SRID different than that of the + # field -- this is only used by `transform` for Oracle backends. + if self.transformed_srid and SpatialBackend.oracle: + sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt) + else: + sel_fmt = '%s' + return sel_fmt + + # Private API utilities, subject to change. + def _check_geo_field(self, model, name_param): + """ + 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 + used. + + 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 + # 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. + elif not isinstance(name_param, list): + raise TypeError + try: + # Getting the name of the field for the model (by popping the first + # name from the `name_param` list created above). + fld, mod, direct, m2m = model._meta.get_field_by_name(name_param.pop()) + except (FieldDoesNotExist, IndexError): + return False + # TODO: ManyToManyField? + if isinstance(fld, GeometryField): + return fld # A-OK. + elif isinstance(fld, ForeignKey): + # ForeignKey encountered, return the output of this utility called + # on the _related_ model with the remaining name parameters. + return self._check_geo_field(fld.rel.to, name_param) # Recurse to check ForeignKey relation. + else: + return False + + def _field_column(self, field, table_alias=None): + """ + 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 + 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), + self.connection.ops.quote_name(field.column)) + + def _geo_field(self, field_name=None): + """ + Returns the first Geometry field encountered; or specified via the + `field_name` keyword. The `field_name` may be a string specifying + the geometry field on this GeoQuery's model, or a lookup string + to a geometry field via a ForeignKey relation. + """ + if field_name is None: + # Incrementing until the first geographic field is found. + for fld in self.model._meta.fields: + if isinstance(fld, GeometryField): return fld + return False + else: + # Otherwise, check by the given field name -- which may be + # a lookup to a _related_ geographic field. + 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 diff --git a/webapp/django/contrib/gis/db/models/sql/where.py b/webapp/django/contrib/gis/db/models/sql/where.py new file mode 100644 index 0000000000..a1a28d9511 --- /dev/null +++ b/webapp/django/contrib/gis/db/models/sql/where.py @@ -0,0 +1,64 @@ +import datetime +from django.db.models.fields import Field +from django.db.models.sql.where import WhereNode +from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend + +class GeoAnnotation(object): + """ + The annotation used for GeometryFields; basically a placeholder + for metadata needed by the `get_geo_where_clause` of the spatial + backend. + """ + def __init__(self, field, value, where): + self.geodetic = field.geodetic + self.geom_type = field._geom + self.value = value + self.where = tuple(where) + +class GeoWhereNode(WhereNode): + """ + Used to represent the SQL where-clause for spatial databases -- + these are tied to the GeoQuery class that created it. + """ + def add(self, data, connector): + """ + This is overridden from the regular WhereNode to handle the + peculiarties of GeometryFields, because they need a special + annotation object that contains the spatial metadata from the + field to generate the spatial SQL. + """ + if not isinstance(data, (list, tuple)): + return super(WhereNode, self).add(data, connector) + alias, col, field, lookup_type, value = data + if not hasattr(field, "_geom"): + # Not a geographic field, so call `WhereNode.add`. + return super(GeoWhereNode, self).add(data, connector) + else: + # `GeometryField.get_db_prep_lookup` returns a where clause + # substitution array in addition to the parameters. + where, params = field.get_db_prep_lookup(lookup_type, value) + + # The annotation will be a `GeoAnnotation` object that + # will contain the necessary geometry field metadata for + # the `get_geo_where_clause` to construct the appropriate + # spatial SQL when `make_atom` is called. + annotation = GeoAnnotation(field, value, where) + return super(WhereNode, self).add((alias, col, field.db_type(), lookup_type, + annotation, params), connector) + + def make_atom(self, child, qn): + table_alias, name, db_type, lookup_type, value_annot, params = child + + if isinstance(value_annot, GeoAnnotation): + if lookup_type in SpatialBackend.gis_terms: + # Getting the geographic where clause; substitution parameters + # will be populated in the GeoFieldSQL object returned by the + # GeometryField. + gwc = get_geo_where_clause(table_alias, name, lookup_type, value_annot) + return gwc % value_annot.where, params + else: + raise TypeError('Invalid lookup type: %r' % lookup_type) + else: + # If not a GeometryField, call the `make_atom` from the + # base class. + return super(GeoWhereNode, self).make_atom(child, qn) |